← Back
functional-programming

Functional core, imperative shell

This post was originally published on MarsBased blog

Photo by Rémi Müller on Unsplash

Photo by Rémi Müller

Yesterday, during our discussion about dependency injection I explained a little bit how functional programming changed the way I've make tests and organize code inside services.

I want to develop a little bit this technique.

Pure functions #

The two most important features of functional programming are function purity and immutable state. I'm going to focus in the first one.

A function is pure basically when:

The first restriction blocks the usage of things like random numbers! 😱

The second restriction block the usage of things like... databases! 😱😱

Some programming languages (Haskell and Elm for example) are 100% pure, meaning only pure functions can be written 😱😱😱

How de hell can we write a program (backend API, for example) that can not use a database!? The solution is simpler than you might think:

First, we write functions that returns a data structure (some times called commands) that describes what the db needs to do. For example, something like:

{ type: 'INSERT', table: 'posts', row: { title: '...', ... } }

That data structure is then converted into actual database calls. Because the function just returns a data structure (instead of changing the world), the function is pure.

The other side is that we receive events from the database indicating what changes had been made. For example:

{ type: 'INSERTED', table: 'posts', changes: { ... } }

Because for a given parameter, our function answers always the same, we can maintain the function purity.

Or in other words, it separates the operations from the side effects.

You can think this is cheating: instead of storing into the database, we specify what we need to store... Ok. It's like cheating. But you have two benefits:

Applying those principles to OOP

Ok. We don't write Haskell (although I'd like to write Elm). How does it fit with our current model?

The idea is the same: isolate side effects from operations.

Yesterday I wrote a simple fictitious example:

def do_something(id, url, user, values)
template = Template.find(id)
page = ApiService.find(url)
content = do_something_complex_with(values, template)
if (content.conditions) CacheService.cache(page.url, content)
value = make_something_complex_with_page(user, values, page)
ApiService.update(page)
value
end

Traditionally, to unit test this code we inject mocks ApiService and CacheService dependencies.

But what FP proposes is to extract the pure parts of the code outside, and unit test only that parts. Something like this:

def do_something(id, url, user, values)
template = Template.find(id)
page = ApiService.find(url)
[data, cache] = PureService.do_something_complex(values, template)
if (cache) CacheService.cache(cache.url, cache.content)
[value, update] = PureService2.do_something_complex(user, values, page)
if (update) ApiService.update(update.content)
value
end

Is typical that the complex logic methods returns two things: the actual computed value, and the effects that need to be applied.

The pattern #

This pattern is sometimes called functional core, imperative shell:

We move all the logic inside pure functions (the functional core). These functions return objects that describes the changes needed to be made.

The imperative shell applies the actual changes (effects) from the description. We keep that code small and trivial. The functional core is tested in isolation (unit testing). Integration tests are used to test the imperative shell.

For example, the video of the previous link shows the test of a class named TweetRenderer. Despite the name, it doesn't render anything: it returns an object describing what to render:

Zoom Screenshot 2020-01-12 at 22.17.02.png TweetRenderer spec 170 KB View full-size Download

That's part of the functional core. The result of that core should be trivial to translate into actual environment changes.

Extensibility #

There's another advantage of using description of changes instead of making changes directly: that descriptions can be (post)processed.

For example, we could return a list of database changes and some code (middleware) could transform that into API cache calls. The business logic is still the same but the effects are different.

Another popular example are React components. They produce a description of the DOM that is diffed against the actual DOM to produce description of DOM changes. Other system uses scheduling (fibers) to apply those changes more effectively.

Conclusion #

This patterns provides a good alternative to split complex logic. Not only you get an easy to test code. By separating what should we do to the actual doing, the code becomes clean, and easy to understand and reason about.

🖖