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.
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"}
]
end
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.
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))
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
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)
end
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))
Sometimes you just want to return {:ok, _}
:
list
|> Enum.map(...)
|> ok()