Railway Oriented Programming in Elixir
It would be a great world if everything ran without errors. Validating inputs from users and handling various other kinds of error ranging from databases access, file access, network issues or any other is a reality of the software. We all have written programs where the cases of handling erros are so much that it increases the codebase by manifold. This make the life of the maintainer a hell, yet this is something that has to be done. Even the most simplest program when changed to handle various kinds of error just makes the code inelegant. I started learning functional programming and it was a breeze to write software without introducing common bugs but error handling is still a reality. Wondering how error handling can be done in an elegant way I stumbled upon a talk by Scott Wlaschin where he describe Railway Oriented Programming (ROP). This is a great design pattern to follow which allows you to create functional programs which handle errors and yet are very readable.
# Code before error handling
request
|> validate_request
|> get_user
|> update_db_from_request
|> send_email
|> return_http_message
# Code after error handling
request
>>> validate_request
>>> (map get_user)
>>> (tee update_db_from_request)
>>> (try_catch send_email)
>>> return_http_message
How to handle errors
A typical application contains some data and some set of operations performed on it. Usually errors in an imperative language are handled by try catch
or checking that the each operation(function) returned as expected and if not then return error. This causes a lot of defensive coding by adding a lot of if
into the code hence making it difficult to reason about.
Now lets take how we can handle errors in a functional paradigm in a better way. Lets say that each function returns either {:ok, "data"}
in case of success or returns {:error, "Failure reason"}
in case of failure. We can call such function two-track functions. Now if we chain such functions then in case of error we dont want to execute rest of the functions but instead bypass them
So a railway metaphor works really well here. You have a railway track which you take in case of success and switch the track in case of failure
Elixir is awesome but it does not have a good support for currying function like Haskell or F#, but it has macros. Lets see how we can create an operator which allows us to switch tracks and chain two-track functions
defmacro left >>> right do
quote do
(fn ->
case unquote(left) do
{:ok, x} -> x |> unquote(right)
{:error, _} = expr -> expr
end
end).()
end
end
In the >>>
operator macro, which we will call the bind operator, we create an anonymous function which checks that if the data is {:ok, _}
then call the next function otherwise bypass calling the function and go to the next step. In this way if there is an error in any place in the track then it will bypass all other functions and at the end we will get {:error, _}
which we can handle.
In this way we can chain multiple such functions and handle the error at the end by logging it or returning the same error.
Other helper macros
Creating two-track functions
Lets face it, not all functions are bad. There are some functions which we can always trust to return a valid thing or do a valid thing and they will never throw errors. We can create another macro which always changes the output from such functions to a success two-track function output.
defmacro map(args, func) do
quote do
(fn ->
result = unquote(args) |> unquote(func)
{:ok, result}
end).()
end
end
Here we call the function and then change the output to {:ok, result}
format so it can be chained with other functions by the >>>
bind operator.
request
>>> name_not_blank
>>> (map name_trim)
In the example name_trim
is a function that calls the String.rstrip
on the name in the request and returns the updated request. We know this will always result in a success hence we create a single-track function to two-track-function by using the map
macro.
Dead-end functions
There are some function which will do something meaningful but wont return anything meaningful that we want to chain with other functions.
request
>>> validate
>>> update_db
>>> send_email
In the example the update_db
function just updates the DB and returns something which we dont want to pass to send_email
function. So what should we do here? We will create a tee
macro which will call the dead-end function and then return the input back as output.
defmacro tee(args, func) do
quote do
(fn ->
unquote(args) |> unquote(func)
{:ok, unquote(args)}
end).()
end
end
In the tee
macro we call the function and return the input back again.
So chaining would be as follows
request
>>> validate
>>> (tee update_db)
>>> send_email
Functions that throw exceptions
Then there are functions that still throw error and dont return the two-track output i.e. {:ok, _}
or {:error, _}
. We will create another helper macro which will change a function which can throw exceptions to a two-track output. Uncreatively I will name it try_catch
.
defmacro try_catch(args, func) do
quote do
(fn ->
try do
{:ok, unquote(args) |> unquote(func)}
rescue
e -> {:error, e}
end
end).()
end
end
In this macro we have a try..rescue
which in case of exception will convert the exception into the {:error, _}
pattern and in case of success will convert the ouput to {:ok, _}
pattern.
Putting it together
Now we have all these macros, lets see how we can chain multiple functions which have different behaviour using the bind operator
We have the following functions to perform on a request from user
- Validate Request (two-track): Function that validates the request values.
- Get User (one-track): Gets the user.
- Update DB (dead-end): Updates the DB and does not return anything that can be chained with other functions hence its a dead-end function whose output we dont care about.
- Send Email (can throw exception): Send email is a standard function that can throw exception.
- Return Http Message (two-track): It converts the function into an appropriate HTTP message.
request
>>> validate_request
>>> (map get_user)
>>> (tee update_db)
>>> (try_catch send_email)
>>> return_http_message
Other helpers
There can be other kind of helper macros/function as well such as a supervisor macro/function which allows you to handle both success and error inputs instead of bypassing error. Think in terms of such helpers and you would be able to make your code more easy to understand and much more maintainable.
What could be next
I think it would be great to have a Hex package that provides all such kind of helper macros & functions to make ROP much easier in Elixir which anyone can leverage in there projects.
Currently I have created a Gist which contains these macros.
Disclaimir: I have started learning metaprogramming in Elixir so there might be better ways of implementing the macros that do the same thing. Let me know what you think about it, will love to hear about it.
References
Railway Oriented Programming - F# for fun and profit
I would recommend to go through the talk if you are interested in ROP. I haven't convered all the things.