Fl4m3Ph03n1x

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:

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.

Last Post!

i-n-g-m-a-r

i-n-g-m-a-r

happy and throw are 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 throw when they are not happy.
Data that is thrown can be formatted in such a way that it is easy to catch just like you would catch an unhappy with outcome using else.
Combining |>, throw and catch can 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.

Where Next? Top

Trending in Discussions Top

cblavier
Hey there, It’s been more than a year since we started using LiveView as our main UI library and building a whole library of UI componen...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New
axelson
Hi there! :wave: @frigidcode and I (but mostly him) have been running an Elixir Book club, we’re almost done with Designing Elixir Syste...
New
achempion
I’ve been using Emacs as my main code editor for more than a two years. It’s a custom build version although I’ve tried doom emacs and sp...
New
budgie
I love Elixir. It’s one of 2 programming languages I’ve ever fallen in love with. But I don’t use it anymore. Serverless was the promis...
New
jtormey
Lately I’ve been thinking about how to organize components as a LiveView application grows. One of the pain points I’ve found (for myself...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New
georgeguimaraes
Just published claude-code-elixir, a plugin marketplace for Claude Code with Elixir support. These are the plugins I’ve been using for my...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews