Functional Core/Imperative Shell

Brian Sung
3 min readNov 11, 2019

--

Designing an application to have a functional core and an imperative shell produces nice side effects, and allows for easy testing. For example, when it comes to testing, you simply need to test the return value on functional pieces, which are often naturally isolated. This allows for testing in isolation and not having to rely on any test doubles. Furthermore, a functional core leads to an imperative shell with few conditionals which make reasoning about the application’s state much easier.

Functional Core

One important thing to remember is that in functional programming, data is immutable, meaning that it cannot be changed. For example, you could do something like this in Ruby:

# in Ruby
arr = [0, 1, 2, 3]
arr[0] = 10
puts arr
# output
10
1
2
3

Data is mutable in Ruby. In the example above, the first element of the array was set to 10 and affects the arr array. The data was changed.

However, in a functional programming language such as Elixir, you cannot mutate data:

# in Elixir
arr = [0, 1, 2, 3]
arr[0] = 10
** (CompileError) iex:2: cannot invoke remote function Access.get/2 inside a match

If you wish to do something similar in the Ruby example, then you would do something like this:

arr = [0, 1, 2, 3]
arr2 = List.replace_at(arr, 0, 10)
IO.inspect(arr2)
# [10, 1, 2, 3]
IO.inspect(arr)
[0, 1, 2, 3]

Notice that the original arr was not changed in any way. The List.replace_at essentially creates a new List. It does not alter the original.

In short, the functional core contains all of the logic and the data manipulation in an application. The functional core also contains many paths, and few/no dependencies, which help isolate it.

When it comes to testing, the functional core is easy to test since the tests are isolated. For example, if you wanted to test a function that pushes a value to a list, then one would expect something like this:

test "it pushes a value to the end of a list" do
list = [0, 1, 2, 3]
value = 4
assert ExampleModule.push(list, 4) == [0, 1, 2, 3, 4]
end
defmodule ExampleModule do
def push(list, value) do
list ++ [value]
end
end

The test itself is very easy to read and is simple!

Imperative Shell

Think of the imperative shell as a blanket that covers the functional core from the outside world. The imperative shell is responsible for actions such as passing data into the functional core, manipulating stdin and stdout, manipulating the database, interacting with the network, etc. All of which are dependent on values passed back form the functional core.

The imperative shell contains few paths, but many dependencies. Therefore, unlike the functional core, the imperative shell is not isolated.

In the example application that Gary Bernhardt goes over in his screencast “Functional Core/Imperative Shell”, he mentions that he does not have any tests for his imperative shell because there are very few conditions in the imperative shell. It contains many flat methods that do not have conditionals, which eases the applications reasoning of state (as mentioned earlier).

The following code highlights something you might find in an imperative shell:

def get_player_move(board, marker) do
input = get_input()
|> String.trim
|> InputValidator.number
|> InputValidator.validate_player_input(board)
case input do
{:valid, position} -> position
{:taken, position} -> position_taken(position, board, marker)
:not_a_number -> invalid_move(board, marker)
end
end

You will notice that there is a conditional in the code. However, it is still fairly easy to test using integration testing. These are the three main integration tests:

  1. The input is valid
  2. The input is invalid due to the position being taken
  3. The input is not a number, therefore invalid

The rest of the tests reside in the functional core (ex. InputValidator) where they don’t require any command line input, etc.

Being still fairly new to functional programming, I couldn’t wrap my head around the concept of functional core/imperative shell at first. I had to rewatch Gary Bernhardt’s screencast a few times (which I highly recommend). It can be found here. This blog post highlights my understanding of the functional core/imperative shell, so it’s not perfect and I am definitely open to feedback/criticism!

--

--