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"}
]
endUse 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)
endWith 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}
endTo 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()
endThe analogous pipeline using next/2 might be:
def something(arg1, arg2) do
arg1
|> next(& fn1(&1, arg2))
|> next(& fn2(&1))
endTransaction 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
endAnd 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)
endAccumulating 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()