2 min read

Using then and then being happy

Using then and then being happy

Elixir is a functional programming language, and thus it encourages the usage of pipes to build a single block of code that represents a pipeline of chained operations through input and output.

Example of such a pipeline:

url
|> fetch_data()
|> parse(format: :raw)
|> map_to_internal_format()
|> filter(& &1.price > 100)
|> apply_efficient_algorithm()
|> map_to_json()
|> send_to_storing_service()

Most of the code can be written as a pipeline, which according to most people is easier to read and understand and it is also pretty concise.

However in some cases pipes can become cumbersome due to mismatch between input and output.


Let's say fetch_data returns a result tuple ({:ok, data} if successful and {:error, reason} if failed). In this case we need to use an anonymous function or case block to adjust the format:

url
|> fetch_data()
|> (fn -> {:ok, data} -> data end).()
|> parse(format: :raw)
...

In the example above we do not match the error case which will mean that it will break and return an exception, which is acceptable for us now.


The order of parameters can also differ. Let's say apply_efficient_algorithm takes an atom as first parameter specifying which algorithm to apply. In this case we again need to adjust:

...
|> filter(& &1.price > 100)
|> (fn input_for_algorithm -> apply_efficient_algorithm(:dijkstra, input_for_algorithm) end).()
|> 
...

Finally we may also need to use other variables/parameters in conditionals.

Let's say we get the storing service name as parameter. Then our example becomes:

...
|> map_to_json()
|> (fn json ->
    case storing_service do
        :mongodb ->
            send_to_mongodb(json)
        :sql ->
            json
            |> build_sql_query()
            |> send_to_postgres()
        :memory ->
            save_in_ets(json)
    end
).()

As we can see our code becomes bloated by these anonymous functions, which can be a bit annoying. To ease on this pain Elixir introduced the function Kernel.then, which simply takes its first argument and passes it to the function given as parameter. This combined with the capture operator & can transform our whole code to:

url
|> fetch_data()
|> then(fn -> {:ok, data} -> data end)
|> parse(format: :raw)
|> map_to_internal_format()
|> filter(& &1.price > 100)
|> then(&apply_efficient_algorithm(:dijkstra, &1))
|> map_to_json()
|> then(
    &case storing_service do
        :mongodb ->
            send_to_mongodb(&1)
        :sql ->
            &1
            |> build_sql_query()
            |> send_to_postgres()
        :memory ->
            save_in_ets(&1)
    end
)

Besides getting rid of those arbitrary anonymous function calls, we can also use captured functions in a neater way, which will also mean that we do not need to think about input variable names so much. Of course we could stop our pipeline and use variables and imperative-like code, but isn't it simpler and more readable this way ?