# 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](https://hex.pm/packages/next_pipe), the package can be installed
by adding `next_pipe` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:next_pipe, "~> 0.1.0"}
]
end
```
## Usage
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.
```elixir
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:
```elixir
with {:ok, value} <- fn1(arg1),
{:ok, value} <- fn2(value, arg2) do
fn3(value)
end
```
With `NextPipe`:
```elixir
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`:
```elixir
try do
with {:ok, value} <- fn1(arg1),
{:ok, value} <- fn2(value, arg2) do
fn3(value)
end
rescue
exception -> {:error, exception}
end
```
To using `NextPipe`:
```elixir
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:
```elixir
def something(arg1, arg2) do
arg1
|> fn1(arg2)
|> fn2()
end
```
The analogous pipeline using `next/2` might be:
```elixir
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`:
```elixir
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`:
```elixir
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
```
## Reducing
Accumulating results from tuple-returning functions often involves the same boilerplate:
```elixir
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:
```elixir
next_while(enumerable, &ExternalSystem.call(&1))
```
## Convenience
Sometimes you just want to return `{:ok, _}`:
```elixir
list
|> Enum.map(...)
|> ok()
```