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:
-
is referentially transparent - it will always return the same output for the same input
-
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:
-
transformData
function transforms (normalizes and prepares) the fetched data for rendering - you can see the returned data as a form of ViewModel - the data is organized in a way that we need for our view and if the outside data format ever changes, we just need to change the mapping function -
render
function takes that model and returns rendered html string - it can call other pure functions which return html parts (i.e. similar to the concepts of components or partial views)
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).
Note: This is a snapshot of the wiki page from the BetterWays.dev wiki, you can find the latest (better formatted) version here: betterways.dev/functional-core-imperative-shell.
Top comments (0)