Fl4m3Ph03n1x

Fl4m3Ph03n1x

Should one use pipes or with?

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:

https://medium.com/@naveenkumarmuguda/railway-oriented-programming-a-powerful-functional-programming-pattern-ab454e467f31

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:

  1. errors get trickled down automatically and returned without me having to manually specify it
  2. I don’t need to manually add a multiclause function to deal with the errors
  3. my function’s signatures are very clean and don’t need to always include the boilerplate {:ok, value} input signature
  4. 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 ?

Most Liked

tme_317

tme_317

I use with for all pipelines where the functions can fail (i.e. return :ok or :error tuples) and we want to break out of the pipeline to deal with it and |> for simpler pipelines which either can never fail or throw when they do. This reduces the use of nested and confusing case or if statements.

Some contrived examples:

def build(params) do
  %User{}
  |> cast(params, [:name, :age])
  |> apply_changes()
end

def convert_to_csv(source_filename) do
    with {:l1, {:ok, :xlsx}} <- {:l1, detect_file_type(source_filename)},
         {:l2, {:ok, csv_filename}} <- {:l2, convert_excel_to_csv(source_filename)} do
  {:ok, csv_filename}
else
  {:l1, {:error, error}} -> {:error, error}  # (such as file not found)
  {:l1, {:ok, _other}} -> {:error, :not_an_excel_file}
  {:l2, error} -> error
end

Note wrapping the tuples in the with statement with their line numbers which I found necessary to disambiguate the else clauses. I’m not sure there’s a better way of doing that without a third-party library.

dbern

dbern

I think René really illustrates well the use-cases of with and |>, and then introduces another idea about creating a token struct to track effects (kind of like Plug) when the pipeline becomes more complicated.

LostKobrakai

LostKobrakai

My biggest issue with not using with is that each function in the pipeline needs to be able to handle all the error cases of the previous function, which makes reordering/restructuring more difficult besides the obvious redundancy.

Where Next?

Popular in Discussions Top

WildYorkies
It seems that the more I read, the more I find Elixir users speaking about all the ways that Elixir is not good for x, y, and z use cases...
New
jeramyRR
This is an interesting article to read. Elixir’s performance, like usual, is excellent. However, it seems like the high CPU usage is co...
New
cvkmohan
The upcoming Phoenix 1.6 release looks very interesting. Became a habit to watch the commits - and - what they are bringing in. phx.gen...
New
AlexMcConnell
The reason that Rails is as popular as it is is because it’s very easy for relatively inexperienced developers to get a lot of work done....
588 19675 166
New
marciol
Please, let me know if this kind of discussion already took place in another topic . Hi all, how do you consider if is better to build ...
New
PragTob
Hey everyone, this has been on my mind for some time and I’d love your input on it! TLDR: I feel like maps are superioer for storing and...
New
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New
MarioFlach
Hello, I want to share a project I’ve been working on for a while: https://github.com/almightycouch/gitgud Background Some time ago I ...
New
Qqwy
Looking at the stacks that existing large companies have used, WhatsApp internally uses Mnesia to store the messages, while Discord uses ...
New

Other popular topics Top

TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
274 42158 114
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New
vegabook
I’m brand new to Phoenix and I have stripped one of the demo applications to the bone. I just want to get an svg up on the screen. Here i...
New

We're in Beta

About us Mission Statement