NextPipe (NextPipe v0.1.0)

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 value. If value 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 value.

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

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

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

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 -> {: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

Link to this section Summary

Functions

Always call always_fn with the pipeline value. Unlike next, the always_fn receives the full tuple passed as the first argument to NextPipe.always/2, i.e., {:ok, value} or {:error, value}.

Conditionally call the next function with the pipeline value.

Conditionally call the next function with the pipeline value and provide an outlet for handling exceptions when next_fn is called.

Link to this section Functions

Link to this function

always(value, always_fn)

@spec always(
  {:ok, any()} | {:error, any()},
  (any() -> {:ok, any()} | {:error, any()})
) :: {:ok, any()} | {:error, any()}

Always call always_fn with the pipeline value. Unlike next, the always_fn receives the full tuple passed as the first argument to NextPipe.always/2, i.e., {:ok, value} or {:error, value}.

always_fn should return a value matching either {:ok, _} or {:error, _}.

Link to this function

next(input, next_fn)

@spec next(
  {:ok, any()} | {:error, any()} | any(),
  (any() -> {:ok, any()} | {:error, any()})
) :: {:ok, any()} | {:error, any()}

Conditionally call the next function with the pipeline value.

If the pipeline value matches {:ok, value}, the next_fn is called with value.

If the pipeline value matches {:error, value}, the call to next_fn will be skipped and the pipeline value is returned unchanged.

Otherwise, the function is called with the value, which supports the use of next/2 at the beginning of a pipeline.

next_fn must return {:ok, value} if it was successful. Otherwise, it must return {:error, value}.

next/2 returns the value returned by next_fn.

Link to this function

try_next(value, next_fn, rescue_fn \\ fn _value, exception -> {:error, exception} end)

@spec try_next(
  {:ok, any()} | {:error, any()} | any(),
  (any() -> {:ok, any()} | {:error, any()}),
  (any(), any() -> {:ok, any()} | {:error, any()})
) :: {:ok, any()} | {:error, any()}

Conditionally call the next function with the pipeline value and provide an outlet for handling exceptions when next_fn is called.

This function generally behaves the same as next/2 unless an exception is raised in next_fn.

In that case, the exception is rescued and rescue_fn is called with two arguments: the second element of the {:ok, value} tuple and the raised exception. The value returned from try_next/3 is then the value returned from rescue_fn. rescue_fn must return a value matching {:ok, _} or {:error, _}.

The default implementation of rescue_fn simply returns an {:error, exception} tuple, where exception is the exception raised by next_fn.