The internet is full of information, mostly related to learning how to do something, but when it comes to software development, I feel the bigger question is why we should learn something. Imagine you are explaining to a friend that if they are confronted by a bear, they should stand their ground, make lots of noise, and try to look as big and scary as possible. Trying to intimidate an animal who could brush you aside with the flick of a claw seems ridiculous, so your friend might decide that it’s better to stick with the “put your knees to the breeze” strategy. Or they might take your advice but use it at the wrong time, like when they are between a mother bear and her cubs. Knowing why, and when, we do certain things allows us to trust that it’s worth doing and, more importantly, have the context to know when it’s appropriate.

At some point, you may have looked at an example of a TODO app written functionally, and decided that this was not for you, or maybe you gave it a try, but it turned into an unmaintainable mess. Without understanding the goal of functional programming, it’s hard to receive the benefits. That’s why I’d like to focus on why we as developers would choose to learn to think in a more functional way. This is not to say that you need to switch to using a functional language immediately, but programming with a functional mindset—choosing to use patterns and architectures that are embraced in functional languages—can give you many of the same benefits.

So, why? Why is it worth your time to change your entire process when you’ve been carrying on just fine until now? Well, for one, a functional codebase is generally a lot more enjoyable to live with and work in. This may surprise you because most of the time we are told that functional programming is confusing, academic, hard to read, etc. And while it’s true that staring at a blank page and producing functional code may require a bit more upfront thought than other programming paradigms, generally the results are much more enjoyable to read and maintain.

How can this be? I think it’s important to differentiate between something that is “basic” versus something that is “simple”. For example, I could use basic words to say “I am sad and mad”, or I could say, “I am exasperated”. Even if the word “exasperated” is slightly more advanced, you better understand how I’m feeling because the word choice is a more accurate and more nuanced description of my emotion. Similarly, functional programming, while it has some concepts that are slightly more advanced, is a more accurate description of the kinds of problems we are trying to solve when we program.

Generally we write code to solve problems or automate tasks that are going to happen in the future. So why do we write code that reads in present tense? Future tense may be a slightly more complex language construct than present tense, but the future is complicated! Future tense is a more accurate representation of our situation.

Consider two ways of giving driving directions:

  1. “Turn left, now turn right, go straight past the blue house.”
  2. “First, you’ll turn left, then you’ll turn right, then keep driving straight until you go past the blue house. If you’re struggling just give me a call.”

The first scenario is written in present tense, and makes the assumption that the roads are not closed, or the blue house hasn’t been painted white. This works fine a lot of the time, but it can give us a false sense of security until we are sitting in front of a “Road Closed” sign, or a freshly painted white house, and have no idea what to do. In the second scenario, the beginning is very similar, but we are writing in future tense which reminds us that we are describing only one possible future, but the thing about the future is that it’s uncertain. We actually have many possible futures, therefore, we end our directions by acknowledging this and offering an alternative course of action (calling for help) if something doesn’t fit with the expected future.

The concept of an uncertain future has nothing to do with functional programming, this is just how the universe works. We can choose to ignore that fact with traditional programming but we risk buggy or confusing code as a result. With functional programming, we need to handle these many possible futures. How we do this depends on the type of problem we are solving. In the case of the driving directions, we only provide the instructions for the expected case, and in all other cases, you simply reach out for help. In the case of a self driving car, if we say “drive through the intersection if the light is green”, we also need to explicitly handle all other cases. Such as, “If the light is red or there’s a person walking, stop!”

The approach taken to defining the future depends a lot on your use case. Languages like Elixir tend to embrace the style of the first “ask for help” example, where you define only the cases that you expect to happen, and in all other cases allow the Erlang runtime to fail immediately and report the problem. This is perfect for a back-end system where you can monitor the logs and fix any unexpected behavior. Elm on the other hand favors the latter “handle all possibilities” example because it is a front-end language, so the priority is a seamless user experience. We are forced to always handle all possible cases, even if they are unlikely. Isn’t this just error handling? Well, it serves the same purpose, but the word “error” suggests that something unexpected happened. When thinking about the “uncertain future” nothing is “unexpected.” It’s a subtle difference, but the point I’m trying to make is that the future is complicated, and rather than trying to describe it with basic language, we should describe it with the appropriate grammar. Readable code is not about using simple language constructs, it is about clearly modelling our intent.

Before we move on completely from this idea of the “uncertain future”, I want to note that not only do functional patterns provide a better language construct, they also allow us to form our code into an easily defined sequential flow. Time always moves forward, and all we have to do is control the flow of logic in one of two ways: funnel down one possible future into a result, or fan out the many possible futures based on a result. These are the main two constructs for functional programming. Conditionals are used to define all the possible futures, and functions are used to describe what happens in any one of those futures. To make it more concrete, conditionals take one input and define many outputs, functions take many inputs and produce one output. As we define a chain of conditions and functions, the scope of possible futures expands and contracts, but it is always moving forwards through time. We never have to think about “going back” and changing something that has already happened.

Hold on, that sounds like I’m saying there’s no way to keep track of state? Well let’s hold that thought, and first talk about the benefits of not having to worry about state within our flow of logic.

Let’s look at another example with two versions of a recipe:

  1. “Whisk 2 eggs, stir in 2 cups flour, and place in oven”
  2. “Requires: 2 Eggs, 2 cups Flour, Oven preheated to 400 degrees
    Whisk the eggs, stir in the flour, and place in the oven.
    Produces 10 cupcakes”

Once again, the first example looks easier at first glance, but think about all the assumptions it makes. What happens if you whisked the eggs, then realize you don’t have any flour, or you finished combining the ingredients but realize your oven needed to be preheated? The instructions could be made a little more clear by saying “Preheat oven to 400 degrees, whisk two eggs from your refrigerator, and stir in 2 cups of flour from your pantry, place in the oven.” This is a little better, but it still has assumptions about the state of the world. It assumes you have those ingredients in your pantry, and that your oven actually works. In the second example, it states up front what is required to complete this recipe, and it states what will be produced by following this recipe. Nothing is assumed. If you don’t have those ingredients and a working preheated oven, you know not to even start this recipe.

Consider a real code example:

eventsByLocation = () => {
    const events = JSON.parse(localStorage.events)
    let groups = {}
    for(const event of events){
      const group = groups[event.location] || []
      group.push(events)
      groups[event.location] = group
    }
    return groups
}

We’ve got two concerning things going on right here. First, by referencing localStorage we are making assumptions about the state of the world within a function so now we can’t always trust that the function will work. Functions that are the deterministic result of their inputs are known as “pure functions,” and while not all functional languages enforce this, it is a common best practice to follow this pattern as much as possible. Second, we’ve built up a lot of local state along the way, such as the groups variable which is mutated while looping through events. We’ve started to blur the intent of our function because now we’re not simply giving linear directions; we are keeping track of things that happened in the past. Let’s try this instead:

eventsByLocation = (events) =>
  events.reduce((acc, event) => {
    const group = acc[event.location] || []
    return {...acc, [event.location]: [...group, event]}
  }, {})

Here we can see that the function is pure. We pass in everything we need, we have no mutable internal state, and everything flows from beginning to end in an elegant way. And this is just using modern JavaScript! With functional languages and libraries this could be a one liner.

But we have to have state somewhere don’t we? Yes, we do. We can never eliminate state completely from a system, but we can keep it from seeping into every part of our application where we have to constantly be managing it. This pattern is called Functional Core, Imperative Shell. The idea is, the core business logic of your application is written with pure functions, in future tense, and is wrapped by a thin layer of code that interacts with the outside world, deals with state, and is written more in the “present tense.”

State is useful, but we want it to stay out of our way, and there is no one way to do it. Elixir leans heavily on Erlang OTP processes, and state is maintained by modifying data in a recursive loop. Elm deals with state using an internal architecture that you cannot modify directly; only send messages that instruct the internal state how you would like it to change. This is an implementation of the CQRS pattern, as is Redux, a common state management library for JavaScript, often used with React. Regardless of the library or the architecture you choose, the main point here is that state and business logic should stay separate so you don’t have to deal with the complexity of uncertain state.

Languages like Elixir and Elm suggest and enforce functional patterns to varying degrees, but the great thing is that you can start wherever you are. Functional is a way of thinking, not a language. For instance, React helped me to start thinking in a functional way. The idea that your UI is simply a function of your state was incredibly powerful, then Redux on top of that helped to decouple state from business logic even more. As I got more excited about these patterns, I started using Ramda, a library which allows for writing functional code more fluently in JavaScript, using function composition, piping, and currying. Finally I started working in languages that were designed to be functional, but honestly, at that point, I had become a functional thinker, so it wasn’t that big of a jump. I hope that my journey can encourage thoughtfulness about the way we define problems and the way we communicate intent.

Eric Newbury

Hash An icon of a hash sign Code Name
Agent 0063
Location An icon of a map marker Location
Washington, DC