Background
Recently I have rediscovered the with
statement to program in a more rail oriented way, and I must say I am loving it thus far.
However in work, my colleagues prefer the usual pipeline oriented approach to railway oriented programming.
My objective here is to discuss the pros and cons of which one and to put my opinions on the table. I am looking forward to reading your opinions and styles on this as well so I can build a better argument to embrace with
or simply part ways with it.
ROP
ROP, or railway oriented programming is not a new concept, but it has been popularized recently with the re-introduction of functional languages. At its most simple stage (the one we will be using here) it boils down to executing X functions in a pipeline, and if a piece of the pipeline fails, it simply carries the error until the end of the pipeline without executing the missing pieces of the pipeline.
You can read a little bit more about it here:
Code
So, in Elixir there are 2 ways of applying this pattern. With with
statements:
def test(x) do
with
{:ok, o1} <- f1(x),
{:ok, o2} <- f2(o1)
do
f3(o2)
end
end
defp f1(x) do
if x > 1 do {:ok, x} else {:error, :too_small} end
end
defp f2(x) do
if x > 5 do {:ok, x+1} else {:error, :not_valid} end
end
defp f3(x), do: x*2
And with pipelines mixed with multiple clause functions:
def test(x) do
x
|> f1()
|> f2()
|> f3()
end
defp f1({:ok, x}) do
if x > 1 do {:ok, x} else {:error, :too_small} end
end
defp f1({:error, _reason} = err), do: err
defp f2({:ok, x}) do
if x > 5 do {:ok, x+1} else {:error, :not_valid} end
end
defp f2({:error, _reason} = err), do: err
defp f3({:ok, x}), do: x*2
defp f3({:error, _reason} = err), do: err
Opinions !!
When comparing the with
version to the pipeline one, I see with
has the following advantages:
- errors get trickled down automatically and returned without me having to manually specify it
- I donât need to manually add a multiclause function to deal with the errors
- my functionâs signatures are very clean and donât need to always include the boilerplate
{:ok, value}
input signature - I write less code
However, the pipeline has the advantage of making the public function test
more readable. It is very clear what the flow of information is when compared to the with
version. This example only has 3 functions, but in pipelines with 10 functions or more (we have those) I am not sure with
would be a winner because I believe it makes the code of the public function quite harder to read. I wish there was a way to make it clearer.
What do you guys think? Are there any other issue/benefits of pipelines VS with ?