DEV Community

Cover image for Making a sandwich with functional javascript
Anton Gunnarsson
Anton Gunnarsson

Posted on • Updated on • Originally published at antongunnarsson.com

Making a sandwich with functional javascript

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 adds 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']
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)