Fl4m3Ph03n1x
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 ?
Trending in Discussions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixirconf-us
- #ai
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #hex
- #security










Showing Posts 23 to 14- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
i-n-g-m-a-r
happyandthroware not mentioned during this discussion, I wonder why that is.It seems to me that expressing
a |> clear |> happy(path)is the most important benefit of using pipes.Functions can be designed to
throwwhen they are not happy.Data that is thrown can be formatted in such a way that it is easy to
catchjust like you would catch an unhappywithoutcome usingelse.Combining
|>,throwandcatchcan help express both the happy path and every unhappy scenario very clearly.Of course I’m not saying that a public api should throw stuff.
axelson
If you’re just simply threading a success result through to the next function then you would want to look at the previously mentioned GitHub - CrowdHailer/OK: Elegant error/exception handling in Elixir, with result monads. · GitHub
(or GitHub - expede/exceptional: Helpers for Elixir exceptions · GitHub if you want something more monady)
dogweather
The
withexamples given above all seem like they could be cleaned up with macro to handle threading the success result back around into the next function. Similar to pipe, actually. Would that be possible? Or are real-life uses not so neat and tidy?hlx
I try to use
withas much as I can and for all other stuff I likesage, sage | HexSee the post: https://medium.com/nebo-15/introducing-sage-a-sagas-pattern-implementation-in-elixir-3ad499f236f6
chulkilee
It’s not about using pipe or not. The fundamental question is how to control flow and where the logic should be placed.
withis good for orchestration func (handling all control flow) calling simple func (returning ok/error tuple, focusing on single job)Note that it’s not all or nothing. For example, you may use pipe for funcs for data transformation or small control flow.
peerreynders
There is no one size fits all … best is highly context sensitive.
Without a library the
with/1pattern demonstrated by @tme_317 is probably the best starting point.Granted it isn’t particularly pretty but it gets the job done and there is some flexibility that goes beyond what the pipe can do.
Now I suspect that this has more to do with your own frustration - “why isn’t this already a solved problem within the language itself”.
Likely because this “problem” doesn’t actually come up all that often.
Erlang introduced
{:ok, result}/{:error, reason}more than likely as a poor mansEither(orResult) type.Given how optimized pattern matching is
:ok/:errortuples are a good enough solution.Putting my
Chat on, I can easily imagine an Erlang programmer cringing at the thought of wasting precious function reductions passing an error value around through function calls just to comply with ROP. The attitude would be to drop everything and return the error value promptly - even if it meant a few more lines of code here and there, as long as it benefitted the runtime budget.The Elixir pipe operator is merely a DevX function application feature that takes the place method chaining in OO languages and is almost as useful as function composition. The pipe operator never meant to take on the
:ok/:errortuple issue.That is really the domain of
with/1. But in order to make it useful beyond just plain{:ok, result}/{:error, reason}values it is also more verbose than a pipe. And finallywith/1will quit at the first sign of trouble and is capable of soaking up all sorts of sins committed by the functions that it calls.The same argument can be made against factoring a 1000 line function into multiple smaller functions. To me those smaller functions add value as long as they are well named and often they tend to make the code more declarative.
I hate trying to figure something like this out:
I find this much easier to reason about:
So when you have a 10 function pipeline (or
with/1) then maybe, just maybe that pipe is spanning multiple, distinct transformations that are just begging to be named for the benefit of future maintainers.tangui
Can’t you raise on unexpected errors?
I personally use
withwhen errors are expected, and pipelines with functions that raise when errors should not happen (the boundary is not always obvious):PragTob
withcan be a true life saver especially when you communicate with other systems, I talk about it here. Basic idea is first validate my own data, then validate with external system(s) then insert locally. Or authentication where things might go wrong at multiple places.|>just shows a transformation of input values to output values to me. I usually don’t expect error handling to take place there but just a smooth transformation.NobbZ
Basically you can say, that regular macros are syntactic sugar. Even those that are created by third party libraries or yourself. SpecialForms though are actual syntax.
Fl4m3Ph03n1x
So, it is a special kind of macro, correct? Or is it something else completely different but it is documented in such a way for users to better understand?