Writing Predictable Elixir Code with Reducers
Writing Predictable Elixir Code with Reducers
This is the first part of a two-part series about maintainable code in Elixir. In this part, we will show how code predictability plays a crucial role in a project's short and long-term health. We will use Elixir's built-in features for this, like the pipe operator, tuples, and with
blocks.
First, we'll explain what predictability is and why it is so important. Then we will go through some tools that Elixir
already has and how you can use them to write better code.
Finally, we'll demonstrate how some simple rules can help developers write code that is easy to read, write, and
maintain.
Let's get started!
On Complexity and Predictability
I still remember when I first started learning about functions in my math class. Things seemed simple and easy to understand:
L=12ρv2SCLL = \frac{1}{2} \rho v^2 S C_L
y(x)=2xy(x) = 2x
One function, one variable, and one simple operation.
But as the years passed and more complex things were added to the basics, the small and easy functions were gone.
I was reading stuff like this:
f(x,y)=αlog⁡(x2y3)θ˘f(x, y) = {\alpha\log({x^2 \over y^3}) \over \u\theta}
...What? Alpha? Theta with an inverted hat?
Eventually, though, after rereading and rewriting these kinds of functions over and over, they also become easy to work with.
In the excellent book The Programmer's Brain: What Every Programmer Needs to Know
About, Felienne Hermans does an amazing job of explaining the
different mechanisms that our brain uses to read, write, and understand code, as well as techniques to perform these
tasks as efficiently as possible.
In short: the amount of new information that our brains can handle is actually
pretty small, and there is not really much we can do about it.
The good news is that our memory is very different from a computer — and we can take advantage of that. With enough
repetition, we eventually start to store patterns in our long-term memory and then use that knowledge to understand
new information faster.
That's why a skill becomes easier with more practice, be it math, playing the guitar, or
programming. The more we make things look and behave the same, the easier and faster it is to absorb them.
Why Should I Make My Elixir Code Predictable?
When it comes to understanding what we read and how well other people will understand the code that we write, being
predictable is essential.
Three key elements make code predictable:

Structure: different blocks of code look and behave the same. That is the main reason it is easy to understand a for loop, even in languages that you've never used before.
Size: smaller blocks (like functions, modules, classes, etc.) are easier to remember.
Simplicity: less moving parts, like function parameters, help our brain keep track of what is happening.

One good example of predictability in the Elixir world is the Plug library because:

It uses behaviours to keep all implementations with the same
structure (expected input and output).
Each plug is expected to be as small as possible since they add latency to the
HTTP response.
They have a pretty simple shape: two functions, each with two parameters.

So, what if we make our entire code predictable?


Elixir itself has a couple of nice tools that, when used correctly, help us create predictable code: its pipe operator and with statement.
Keep in mind that neither of
these will enforce a pattern or design principle — they are just tools.
Elixir's Pipe Operator
The pipe |> operator is an amazing tool that helps us express a chain of function calls as a simple sequence of
actions.
Even if you've never written any Elixir code, you probably understand what this piece of code is trying to do:

It's simple to read and easy to understand what is happening.
However, because it's so simple, we're also very limited in what we can do with it. Since all functions are chained, they
depend on the previous result. If any of the functions break, there's not much we can do about it unless we add
error handling to all of them.
For a proper pipeline where we can handle errors and don't want to make the functions dependent on one
another, it's better to use the with statement.
Elixir's with Statement
Pipe operators are simple, but we often need to check the return to ensure we have a valid state.
Let's take the
previous example and rewrite it with a with block:

Now we have proper error handling and a chance to do something if our user data is invalid or it's not
possible to insert the user. However, even in this simple example, it's a bit harder to read because of the
added complexity. Note that since functions can return any value, we have to handle the return of each step explicitly.
Let's use a more complex example — a generic checkout code where we need information about the user, payment, and address — and
try to create an order:

Not only does each function have a different return type, but errors may also have different shapes.
Also, because the existing
functions don't follow a pattern, a developer adding a new step to this feature will not know what the new function
should return. A struct? ok/error tuple? A non-nil/nil value? What about errors?
The lack of predictability here is bad
for whoever is reading this code, as well as people who will modify it in the future.
Design Better Pipelines in Elixir
As previously stated, to achieve predictability, we need three things in our code: structure, size, and
simplicity. Knowing that, we can create a few rules to help us write predictable code:

Each function on a pipeline is expected to transform a given state into an updated state, and it should only do
one transformation. This will help us keep functions small and focused.
Always receive two parameters. The first parameter is the state we want to transform, and the second contains any optional or extra data we might need to perform such a transformation. The second parameter is optional.
Always return an ok/error tuple. If everything goes well, an updated value of a state is returned.

For now, we won't require a structure or type for the error description.
Let's revisit our checkout code, now applying these rules:

It definitely looks better, and if someone in the future has to add a new step, they will know how this new function
should look. Because the errors always have the same shape, we just need to find the appropriate message for a
given error description, and everything else will look the same.
However, although we've solved the design of one particular feature, none of these changes or design principles are
enforced. A team working on a different part of your product might never have contact with this code and may follow
another design or pattern — or none at all.
If we can find a way to enforce these changes, then the code will always look the same.
An Emerging Pattern
Now we can see a pattern emerging when we write our functions: chain-of-state
transformations — a pipeline. It's almost like an Enum.reduce/3 function.
However, instead of applying the same function to a collection of values, we apply a collection of functions to transform a state.
We'll explore this
pattern in greater detail in the next part of this series.
Coming Up Next Time: Enforcing Predictable Elixir Code
In this article, we showed how to write code in a pattern that our brains can understand faster and more
accurately. And by doing so, we also can write — and keep writing! — code that is maintainable in the long term.
Note that this pattern does not cover module organization, project structure, or API design. For that, I highly
recommend the series of articles Towards Maintainable
Elixir by Saša
Jurić.
In the next and final part of this two-part series, we'll learn how to create a basic framework to enforce design principles in our Elixir code.
Until then, happy coding!
P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, subscribe to our Elixir Alchemy newsletter and never miss a single post!