Functor Flavoured Pipes in Elixir

(Even More) Functional Elixir — 2

In the last article, we explored some functional concepts such as currying and composition. And how to apply them to Elixir. Reading through the same book, Professor Frisby’s Mostly Adequate Guide to Functional Programming, another concept draw my attention: functors.

According to the same book above:

A Functor is a type that implements map and obeys some laws. It is simply an interface with a contract.

The values are isolated in a “container”. When we map a function, it will run inside the container, and the effects are isolated in that container. I know, it may not make sense without some prior functional experience. So let’s see some examples.

Maybe nil ?

I’m sure not only once you had to deal with something like this:

The example can be applied to a database query, to an API response, etc. As long as the map is something like %{name: "john"}, everything will be fine. But what happens if the map is something different, such as %{age: 25}, or even worse nil? Our function will throw an error. Not a big deal, right? We can wrap the String.capitalize() in its own function and check for nil before applying capitalize/1. But now imagine your pipe is not 2, but 10 functions long. You will need to change each one to check for null, break the pipe in smaller “segments”, or even chain the functions 😢.

This is where the Maybe container can be useful. Let’s see a basic implementation in Elixir to understand the concept.

Our Maybe container is a struct, with a single value that can be anything, any type.

maybe_of/1

The first function, maybe_of/1 adds the value to the container.

iex > maybe_of("x")
%Maybe{value: "x"}

map/2

A functor implements a map, which is our second function. Think of it as Enum.map/2. It does not receive an enumerable, but a %Maybe{} container, and applies a function to its value if it’s not nil.

iex > map(%Maybe{value: 1}, &(&1 + 1))
%Maybe{value: 2}
iex > map(%Maybe{value: nil}, &(&1 + 1))
%Maybe{value: nil}

maybe/3

The last function maybe/3 retrieves the value from the %Maybe{} container and applies a function to it. If you want to return the value you can apply &(&1). The map checks as well if the container’s value is nil and in that case, returns a predefined (default) value.

Now we can refactor our capitalize_name/1.

As you can see in the doctests, the function is capable of handling wrong and nil values and return a nice error message. You can find the implementation and examples on GitHub.

In theory, it is an interesting example. However, if you want to integrate it into your projects it has some flaws:

  • it’s verbose, introduces a lot of specific language
  • you need to wrap all functions in the pipe in anonymous functions
  • handles the nil case. But what happens if one function along the pipe outputs a different kind of error. Something that the next function in the pipe will not know how to process

Let’s try to use some of the learnings from the functor exercise above, in a more generic and “Elixiry” implementation.

Fault Tolerant Pipe / FTPipe / ~>>

We implement the FTPipe module, with an ~>> operator, which behaves like |>, but with some of the advantages detailed above. We give up the container/struct itself but keep the map concept.

Also, we want it to be generic enough to handle multiple error cases and to be able to use it project-wise. It turns out not to be very complicated to implement:

With a macro and quote/unquote we are able to pass the left and right side of the FTPipe in the function. No need to call anonymous functions anymore.

Then inside the case statement, we look for known errors. It is up to you how specific or generic you want to make it. For example, if you have only one FTPipe in your project, you need a generic error for nil case. But you can still match on something specific like {:error, %Ecto.Changeset{}}.

If no error matches it applies the normal |> pipe between the left and the right side.

Time to refactor again our small example:

It’s almost identical to the first implementation, but with the behaviour of the second one (and even more flexible, as we are able to handle any {:error, _}). Now we use ~>> instead of |>.

Example

As we now have this small but powerful tool, let’s put it to work with a final, more complex example. Sticking to the example in the previous article, we will build a small app that fetches crypto currencies prices. It stores the prices in the database and sends data to the frontend. For the purpose of this exercise, we will mock everything. No real endpoint or database. Ecto and HTTPoison are mocked as empty structs.

We will use the api_key to control different cases for our example.

  • get_price/1 mocks an HTTPoison request that can be either successful, return :ok with a 401 status code, or return error
  • update_price/1 updates the price in the database, may return an Ecto error if the price is missing, or error again if we do not support the provided currency

Update the FTPipe with the new error types.

As said before, the level of detail is up to you. You can match on generic {:error, _} or detail the type of expected error.

Yet, it is very important that you know what errors your app can return. Eg. our solution does not cover something like {:error, _, _}.

Also, worth noting that you can match any kind of result that should not be included in the pipe transformation. Eg. the HTTPoison :ok response, where we match on the status_code: 401.

Let’s add the doctests to our example and see what happens.

We try our different scenarios, and the tests pass. What is more interesting, is to compare our ~>> example implementation with a normal |> pipe.

The first 3 tests will pass as well. But look for the IO.puts statement. It will send the results to the frontend even if they are errors (test 2 and 3). While the FTPipe will send only the result of test 1 to the frontend.

Conclusion

With just a few lines of code added in a module, you can write your own fault tolerant pipe. If you like to use the pipe a lot to transform the data in your application (and I personally do), the FTPipe approach can be useful.

Of course, you can name it anything you want, or change the ~>> symbol. You can even define many FTPipe modules and use them in different parts of the application if you need different kind or error outputs.

As always, I would be very happy to see your opinion on the FTPipe. If you see any value in using it, or even if you think is not such a good idea, please leave a comment.