Elixir Pipes is an Elixir extension that extends the pipe (|>) operator through macros.
Some of the best programmers who have taken an early dive into Elixir have mentioned the pipe as one of the key features of the language. It allows a clear, concise expression of the programmer's intent:
def inc(x), do: x + 1
def double(x), do: x * 2
1 |> inc |> double
The return value of each function is used as the first argument of the next function in the pipe. It's a beautiful expression that makes the intent of the programmer clear.
Sometimes, you need to compose functions with a different strategy. Say your functions use Erlang-style APIs. You might have functions that return {:ok, value}
or {:error, value}
. Then, the pipe operator might make things difficult. After you receive an error
code, you probably want the pipe to stop.
Elixir-Pipes allows you to specify a strategy, in one concise space, that you can then apply to all segments in an Elixir pipe. This capibility will help you compose many different types of functions. How many times have you wanted to:
- compose a pipe that uses some variation of a function call like
[1, 2, 3] |> add(1) |> times(2)
? - halt the execution of a pipe on error?
- tease nils to empty strings, without changing your original funcions?
- transform exceptions to Erlang-style
{:error, x}
tuples?
The recipes are all there waiting for you.
All you need to do to get started is to add the project to your mix file as a dependency. Then, when you want to use the macros, you'll simply use it:
use Pipe
...
That's it. After that, you can continue to use unadorned pipes, or use one of the prepackaged compositions. Initially, we have three:
This function will compose as long as the computed value matches the value so far. For example, consider this Russian Roulette application:
defmodule RussianRoulette do
def click(acc) do
IO.puts "click..."
{:ok, "click"}
end
def bang(acc) do
IO.puts "BANG."
{:error, "bang"}
end
end
pipe_matching {:ok, _},
{:ok, ""} |> click |> click |> bang |> click
...would produce...
click...
click...
BANG.
It would evaluate functions as long as the accumulator matched the expression. In this case, we process statements as long as the composition yields an :ok
on the left hand side.
Sometimes, you may want to test on something other than a match. This composition strategy will continue as long as your composition satisfies the test function you provide. To implement the above, you could do this just as well:
def while_test({:ok, _}), do: true
def while_test(_), do: false
def inc({code, x}), do: {code, x + 1}
def double({code, x}), do: {code, x * 2}
pipe_while(&while_test/1, {:ok, ""} |> click |> click |> bang |> click )
You could also write tests for testing a value, such as whether a value is even, whether a record is valid, or whether a user is authorized.
This will execute the pipe, wrapping each segment (but the first) with the wrapper_fun.
pipe_wrapping &fun/1,
0 |> fun2 |> fun3
is like:
fun.(fun3(fun.(fun2(0))))
Can be useful to precess the answer of a series of API calls for instance.
This function will execute the functions of the pipe without passing any new argument, and merge each result with the merging function. The pipe_accumulate_matching will keep doing this as long as the expression matches, the same way as pipe_matching. The first value can be seen as the initialization of the merge data structure.
As an example, imagine you want to make a few API calls, and merge result while the call are successful, but return the error as soon as one fails:
pipe_accumulate_matching x, {:ok, x}, &Map.merge/2,
%{} |> API.get_user_data(123) |> API.get_avatar(123)
Sometimes, you want to write the composition rules yourself. You can do this with pipe_with function, pipe
where function has a sig of f(x, pipe_segment)
where pipe_segment
is a function in the pipe. The macro will pass the accumulated value and a function that wraps each pipe segment to your function.
Say you have a list, and you want to do arithmetic on each element of the list. You can do so with pipe_with
like this:
def inc(x), do: x + 1
def double(x), do: x * 2
pipe_with fn(acc, f) -> Enum.map(acc, f) end,
[ 1, 2, 3] |> inc |> double
This returns
[(1 + 1) * 2, (2 + 1) * 2, (2 + 2) * 2]
or
[4, 6, 8]
You could also wrap exceptions, and translate them to the form {:error, acc}
, or change nils to blank strings or empty arrays.
You can also use a do end syntax for elixir-pies, like:
pipe_matching {:ok, _} do
{:ok, ""} |> click |> click |> bang |> click
end
Sometimes, it can be useful to perform a function on each segment of the pipe, before it gets passed or accumulated. All pipe macros of maximum arrity accept a wrapper_fun before the pipe.
You can use this to format API call response before the gets accumulated for instance:
pipe_accumulate_matching x, {:ok, x}, &Map.merge/2, &API.format_response/1 do
%{} |> API.get_user_data(123) |> API.get_avatar(123)
end
Contributions are welcome. Just send a pull request (you must have tests).