This post was originally published on my blog.
A while ago I held a lightning talk about functional programming in javascript, and demonstrated it by using the array functions map
, filter
and reduce
to create a tasty sandwich. This post is the written version of that talk, but with a little more background about functional programming. But what is functional programming more exactly? My first hit on Google for "what is functional programming" is this post by Eric Elliot. In it he says this:
Functional programming (often abbreviated FP) is the process of building software by composing pure functions, avoiding shared state, mutable data and side-effects.
Let's take some time to explain these terms before we start making our sandwich.
Pure functions
A pure function is a function that given the same input always returns the same output, and has no side effects.
A very basic example of a pure function:
const add = (x, y) => x + y
This function has two parameters that are added together with the plus operator. No matter how many times we call this function with the same arguments, it will always return the same output.
An unpure function could look like this:
const z = 5
const add = (x, y) => x + y + z
This function depends on state that is shared between scopes, which means that if we change the variable z
but not the arguments we pass to the function the output will change and hence the function is unpure.
Side effects
Side effects are when a function interacts with something outside it's scope. This could be anything from printing something to the console to modifying a variable outside the function.
Some examples of side effects are:
- Modifying any external variable
- console.log()
- Making a HTTP request
- Updating the DOM
- Calling any other function with side effects
This also makes it obvious that not every function can be pure, and that is fine. The benefit of writing pure functions is that they are very easily testable and makes it safer to refactor code since you know that this function won't have any unintended side effects.
Mutable data
A mutable variable is a variable which value can be changed after it's been created. Mutable variables can make it hard to reason about our code since we can't be sure what the variables value is when we use it. On the other hand, an immutable variable is a variable that can't be changed after creating it.
In my opinion the biggest benefit of striving for immutability is that it increases the predictability of our code since mutation hides change. This means we can reason about our code easier, debug it faster and keep the mental overhead small.
It's important to remember that, in javascript, variables declared with const
are not immutable. It only prevents you from reassigning and redeclaring the variable. This would work:
const person = {
name: 'Anton'
}
person.name = 'Alfredo'
To prevent this from working we could use Object.freeze() to freeze the object which will prevent setting the value of name (and throw an error if running in strict mode).
Shared state
Shared state is variables or other state that is shared between between different scopes. For example a functions local scope and the global scope. In functional programming we try to avoid shared state and instead rely on our immutable data structures and the possibility to obtain new data from the existing data.
We've already seen an example of shared state in the unpure function example above. Let's revisit it:
const z = 5
const add = (x, y) => x + y + z
In this example z
is shared by the global scope and the local scope of the function add
. If we change the value of z
it will affect both the global scope and the value inside of add
s scope.
There is a lot more nitty gritty details to each of these terms and you could easily stumble down a rabbit hole of mathematical definitions, but if you want to read more the previously mentioned article is an excellent starting point.
Making a sandwich
Phew! With that out of the way, let's make a sandwich! To get started we need some ingredients, and in the name of this tutorial it will only be the toppings, and only toppings that we can slice.
const ingredients = ['cucumber', 'tomato', 'sallad']
The first step in making our sandwich is to slice our ingredients, which in other words mean we transform the ingredients to sliced ingredients. To transform the elements of our ingredients-array we will use a function called map
.
map
takes one single argument which is a callback function that will be called on every element of the array. The return value of the callback function will be the new value of the element if the new array. We start by creating a function slice
that takes a single ingredient and transforms it to a sliced ingredient. We then pass this function as the callback to map
:
const ingredients = ['cucumber', 'tomato', 'sallad']
const slice = (ingredient) => {
return `sliced ${ingredient}`
}
const result = ingredients.map(slice)
console.log(result)
// output: ['sliced cucumber', 'sliced tomato', 'sliced sallad']
In this case we only use the arrays element in the callback function passed to map
, but the function also has two optional parameters. The first one is the current index of the element and the second is the array. Remember that since map
is a pure function it doesn't mutate the initial array but instead creates a new one, so the array parameter will never change when you run map.
Assembling the sandwich
Let's continue by assembling the sandwich with reduce
.
It is arguably the most powerful of the sandwich making functions. It can be used to accomplish anything from summing some values to running promises in sequence.
The function has two parameters: A callback function (called reducer) and an initial value. When calling reduce
it will enumerate ("loop") through the elements in the array and apply the callback function to each of the elements, finally resulting in a single return value. Let's walk through the callback function arguments:
-
accumulator: The first parameter is named accumulator because it "accumulates the callback's return values". This never made much sense to me as a non native english speaker until I started to think about it as the total or sum of the
reduce
call. This will contain the return value from when the previous element was processed by our callback function (or initialValue, see below). - currentValue: This is the current element that is being processed by our callback.
- currentIndex (optional): The current elements index in the source array.
-
array (optional): The source array. (Remember that since
reduce
is a pure function it doesn't change the source array, so this will not change in any way during the execution of reduce).
The second parameter of the callback function is the initialValue. The accumulator is initiated with whatever value we pass to this parameter.
Alright, now that we know about reduce
we can assemble our sandwich and since map
returns an array we can chain the call to reduce
, making our code more compact and legible:
const ingredients = ['cucumber', 'tomato', 'sallad']
const slice = (ingredient) => {
return `sliced ${ingredient}`
}
const reducer = (total, current) => {
return `${total}, ${current}`
}
const result = ingredients
.map(slice)
.reduce(reducer, 'A tasty sandwich with')
console.log(result)
// output: 'A tasty sandwich with, sliced cucumber, sliced tomato, sliced sallad
Above we call the reduce
function with out callback function reducer
which returns the total
(the accumulator) concatenated with the current
value. This will give us a string representing our sandwich containing the intialValue ("A tasty sandwich with") and each of our sliced ingredients. The output looks a little malformated and we could fix this by utilizing the index and array parameters of the callback function to remove unneccesary commas etc, but for the case of simplicity let's leave it like this for now.
I'm allergic to tomatoes :(
But what if we are allergic to tomatoes? Let's remove it with filter
.
filter
takes a single argument that is a callback function (just like map
), and the callback function has three parameters (the element, the index and the array). The return value of the callback must be a bool indicating whether or not the current element should be included in the new array. In our case this means that we check if the current ingredient isn't tomato and in this case we return true.
const ingredients = ['cucumber', 'tomato', 'sallad']
const slice = (ingredient) => {
return `sliced ${ingredient}`
}
const reducer = (total, current) => {
return `${total}, ${current}`
}
const result = ingredients
.filter(ingredient => {
return ingredient !== 'tomato')
}
.map(slice)
.reduce(reducer, 'A tasty sandwich with')
console.log(result)
// output: 'A tasty sandwich with, sliced cucumber, sliced sallad
In this case I also chose to inline the callback function in the filter
call but this is mostly a matter of preference.
And that's it! We've made a "sandwich" with functional javascript!
🎉
This is a really contrived example that doesn't really demonstrate the power of these functions, but hopefully it gave you some insight into the world of functional javascript. Just remember that you don't have to care about pure functions, immutability or any other confusing term to start benefiting from map
, reduce
and filter
.
You just have to use them.
Top comments (0)