Conditional pipe operator on error tuple

I wanted to share this code for a problem that regularly comes up and that I usually solve with multi-headed functions or with clauses. The issue: handling an error tuple in a pipeline situation by skipping steps.

Here’s the code: (Important: There’s a bug in this macro, see below)

 defmacro left ~> right do quote do case unquote( left ) do { :error, _ } = err -> err val -> unquote( Macro.pipe( left, right, 0 ) ) end end end 

Which makes possible a pipeline that looks like:

start_value |> step_a ~> step_b ~> step_c |> step that handles { :error, _ } or a real result 

Any thoughts on this approach?

Dan

That’s pretty common, what what the exceptional library on hex.pm does (it handles raw values, tagged ok/error tuples, returning exceptions, multiple styles of handling errors, etc…). I use it quite a lot.

3 Likes

While exceptional library is great, this partucular issue is fully covered by Elixir core Kernel.SpecialForms.with/1.

5 Likes

This seems to fix the problem:

 defmacro left ~> right do quote do case unquote( left ) do { :error, _ } = err -> err val -> val |> unquote( right ) end end end 

This is the Kernel.|>/2 macro sequence. It felt a little unsatisfying knowing it translates into Macro.pipe. With a little more time and testing it seems to cleanly reduce to:

unquote( Macro.pipe( (quote do: val), right, 0 ) ) 

A simple test looking like:

iex(101)> 14+7 ~> IO.inspect(label: 1) |> IO.inspect( label: 2) ~> IO.inspect( label: 3 ) |> IO.inspect( label: 4 ) case(14 + 7 ~> IO.inspect(label: 1) |> IO.inspect(label: 2)) do {:error, _} = err -> err val -> IO.inspect(val, label: 3) end case(14 + 7) do {:error, _} = err -> err val -> IO.inspect(val, label: 1) end 1: 21 2: 21 3: 21 4: 21 21 

The code printing comes from an inline Macro.to_string( ast ) for illustration. The code shows backward since the unpipe rolls the pipe up in reverse while nesting.

This is the error tuple scenario:

iex(102)> { :error, 14+7 } ~> IO.inspect(label: 1) |> IO.inspect( label: 2) ~> IO.inspect( label: 3 ) |> IO.inspect( label: 4 ) case({:error, 14 + 7} ~> IO.inspect(label: 1) |> IO.inspect(label: 2)) do {:error, _} = err -> err val -> IO.inspect(val, label: 3) end case({:error, 14 + 7}) do {:error, _} = err -> err val -> IO.inspect(val, label: 1) end 2: {:error, 21} 4: {:error, 21} {:error, 21} 
1 Like