Skip to content

Make Elixir pipelines a bit more flexible by skipping or always calling functions

License

Notifications You must be signed in to change notification settings

objectuser/next_pipe

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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"}
  ]
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.

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)
end

Reducing

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()

About

Make Elixir pipelines a bit more flexible by skipping or always calling functions

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages