Functional core, imperative shell

📄 BetterWays.dev wiki page | 🕑 Last updated: Aug 29, 2022

I'm going to assume that you're at least somewhat familiar with basic functional programming concepts, but as a quick reminder:

A pure function is a function that:

  1. is referentially transparent - it will always return the same output for the same input

  2. have no side effects - it doesn't affect the outside world

This brings us a lot of benefits (referential transparency, unit testing, they are easier to reason about) and we should split as much functionality as possible into pure functions, but how do we get there?

One technique that can help us get there is called the "Functional core, imperative shell". As the name says, the idea is to separate the effectful, imperative code into the outer shell and keep the inner core pure (functional core).

Purely functional languages will nudge you in that direction due to their design (i.e. IO monads), but you can use and benefit from this technique in pretty much every programming language.

If you're reading this guide, there's a good chance that you know at least some JavaScript. So, we're going to go with that.

In some ways, JavaScript is also kind of nudging us in this direction. You've probably used async functions. While they are generally nicer to use than callbacks or promises directly, you soon realize that they don't really play nicely with the rest of your codebase - values inside promises are a very different beast from normal values, and functions which return promises are very different from normal functions.

This is the important part. Instead of trying to mix those two concepts throughout our codebase (and infect almost everything in the process), the idea is that we take and isolate those async (and impure) stuff into the outer shell, so we can keep our core nice and clean.

Here's a very simple example that should look familiar if you ever used something similar to express.js:

// imperative shell
async function handler(req, res) {
    var users = await fetchUsers()
    var projects = await fetchProjects()

    var result = render(transformData(users, projects))

    res.send(result)
}

// functional core
function transformData(users, projects) {}

function render(model) {}

So, we have two parts:

Imperative shell

handler is an express-like request handler function, which represents our outer imperative shell. You can do pretty much what you want here - usually, you need to first get the data from somewhere (impure part), then transform data (pure part), then send the result (impure part).

Functional core

transformData and render are pure functions - you should have all the needed data ready before calling into the functional core, as these functions will use only what they'll get in the parameters.

This example is arbitrary, but here are a few points which may help you to understand it:

As you can see, the idea itself is very simple, but it can take some effort (depending on the nature of the problem you're solving - i.e. IO-heavy code is harder to organize this way).


Comments and suggestions

You can use this form to submit a suggestion or a comment.

If you find this site useful, please consider supporting it. Supporters also get access to some extras.