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

  1. Validate Request (two-track): Function that validates the request values.
  2. Get User (one-track): Gets the user.
  3. 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.
  4. Send Email (can throw exception): Send email is a standard function that can throw exception.
  5. 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.