NextPipe
Make pipelines a bit more flexible by skipping or always calling functions.
There is no use of macros or operator overloading. Just modules and functions.
Installation
NextPipe is available in Hex, the package can be installed
by adding next_pipe to your list of dependencies in mix.exs:
def deps do
[
{:next_pipe, "~> 0.1.0"}
]
endUsage
Use import NextPipe to make your pipelines a bit more flexible.
NextPipe allows the chaining of functions with control through the idiomatic
{:ok, _} and {:error, _} tuples. In the case of a function returning a value
matching {:error, _}, the pipeline is short-circuited.
value
|> next(fn ...)
|> next(fn ...)
|> try_next(fn ..., fn ...)
|> always(fn ...)NextPipe doesn’t use macros or overridden operators. Like Kernel.then/2, the
functions in NextPipe work with function arguments and idiomatic tuples,
{:ok, _} and {:error, _}.
Use next/2 to conditionally execute its function argument based on the first
argument. If the first argument matches {:ok, _} the function passed to
next/2 will be called with the second element of the tuple. If value matches
{:error, _}, the function will not be called and the same tuple will be
returned.
Otherwise (like at the beginning of a pipeline), the function will be called with the first argument.
try_next/3 works like next/2 but rescues exceptions. It accepts a third
optional argument, which is the function to be called in case an exception is
rescued.
Use always/2 to always call the function argument, but with the full pipeline
value, not just the second element of the tuple.
As an alternative to with
The with special form is often use to conditionally call functions if prior
functions are successful:
with {:ok, value} <- fn1(arg1),
{:ok, value} <- fn2(value, arg2) do
fn3(value)
end
With NextPipe:
arg1
|> next(& fn1(&1))
|> next(& fn2(&1, arg2))
|> next(& fn3(&1))
Just like when using with, when creating a pipeline using next/2, if a
function returns {:error, _}, the subsequent functions passed to next/2 are
skipped, effectively short-circuting the pipeline.
If one of the functions may raise an exception, more boilerplate code is eliminated.
Compare using with:
try do
with {:ok, value} <- fn1(arg1),
{:ok, value} <- fn2(value, arg2) do
fn3(value)
end
rescue
exception -> {:error, exception}
end
To using NextPipe:
arg1
|> try_next(& fn1(&1))
|> try_next(& fn2(&1, arg2))
|> try_next(& fn3(&1))Functions with multiple arguments
The function passed to next/2 et al accepts a single argument. If multiple
arguments are required, return a new function with those arguments bound.
As an example, consider the following traditional Elixir pipeline:
def something(arg1, arg2) do
arg1
|> fn1(arg2)
|> fn2()
end
The analogous pipeline using next/2 might be:
def something(arg1, arg2) do
arg1
|> next(& fn1(&1, arg2))
|> next(& fn2(&1))
end
As an alternative to Ecto.Multi
Transaction control with Ecto.Multi is quite powerful and flexible. It can,
however, be a bit cumbersome for simpler situations. And then
Repo.transaction/2 with a simple function requires some boilerplate code for
rescuing any exeptions if passing those up is undesirable. NextPipe may clean
those cases up a bit.
Compare this use of Repo.transaction/2:
def something(arg1, arg2) do
try do
Repo.transaction(fn repo ->
arg1
|> fn1(arg2)
|> fn2()
end)
rescue
exception ->
repo.rollback(value)
{:error, exception}
end
end
And then using NextPipe:
def something(arg1, arg2) do
Repo.transaction(fn repo ->
arg1
|> try_next(& fn1(&1, arg2))
|> try_next(& fn2(&1))
|> always(fn
{:error, value} -> repo.rollback(value)
value -> value
end)
end)
endReducing
Accumulating results from tuple-returning functions often involves the same boilerplate:
Enum.reduce_while(enumerable, {:ok, []}, fn item, {:ok, results} ->
case ExternalSystem.call(item) do
{:ok, result} -> {:cont, {:ok, [result | results]}}
{:error, error} -> {:halt, {:error, {error, results}}}
end
end)
The next_while/2 function captures that:
next_while(enumerable, &ExternalSystem.call(&1))Convenience
Sometimes you just want to return {:ok, _}:
list
|> Enum.map(...)
|> ok()