Arrows (arrows v0.2.1)

A handful of (mostly) arrow macros with superpowers.

Installation

The package can be installed by adding arrows to your list of dependencies in mix.exs:

def deps do
  [
    {:arrows, "~> 0.2.0"}
  ]
end

Or via git:

def deps do
  [
    {:arrows, git: "https://github.com/bonfire-networks/arrows", branch: "main"}
  ]
end

Documentation

The Elixir |> ("pipe") operator is one of the things that seems to get people excited about elixir. Probably in part because you then don't have to keep coming up with function names. Unfortunately it's kind of limiting. The moment you need to pipe a parameter into a position that isn't the first one, it breaks down and you have to drop out of the pipeline format or write a secondary function to handle it.

Not any more! By simply inserting ... where you would like the value to be inserted, Arrows will override where it is placed. This allows you to keep on piping while accommodating that function with the annoying argument order. Arrows was inspired by an existing library.

Examples

# Standard first position pipe
iex> 2 |> Integer.to_string()
"2"

# Using ellipsis for explicit placement
iex> 2 |> Integer.to_string(...)
"2"

# Using ellipsis to place the piped value in a non-first position
iex> 3 |> String.pad_leading("2", ..., "0")
"002"

# Using the ellipsis multiple times
iex> 2 |> Kernel.==(..., ...)
true

# Nested expressions with ellipsis
iex> 2 |> String.pad_leading(Integer.to_string(...), 3, "0")
"002"

A few little extra features you might notice here:

  • You can move the parameter into a subexpression, as in 2 |> double_fst(double(...), 1) where double will be called before the parameter is passed to double_fst.
  • You can use ... multiple times, substituting it in multiple places.
  • The right hand side need not even be a function call, you can use any expression with ....

Ok-pipe

Arrows also provides an ok-pipe operator, ~>, which only pipes into the next function if the result from the last one was considered a success. It's inspired by OK, but we have chosen to do things slightly differently so it better fits with our regular pipe.

inputresult
:-----------------------:--------------
{:ok, x}fun.(x)
{:error, e}{:error, e}
nilnil
x when not is_nil(x)fun.(x)

In the case of a function returning an ok/error tuple being on the left hand side, this is straightforward to determine. In the event of {:ok, x}, x will be passed into the right hand side to call. In the event of {:error, x}, the result will be {:error, x}.

We also deal with a lot of functions that indicate failure by returning nil. ~> tries to 'do what I mean' for both of these so you can have one pipe operator to rule them all. If nil is a valid result, you must thus be sure to wrap it in an ok tuple when it occurs on the left hand side of ~>.

|> and ~> compose in the way you'd expect; i.e. a ~> receiving an error tuple or nil will stop executing the rest of the chain of (mixed) pipes.

Documentation can be found at https://hexdocs.pm/arrows.

Summary

Functions

Error-coalescing operator.

Extracts values from OK tuples.

Wraps a value in an OK tuple if it's not already in a result tuple format.

Wraps a value in an OK tuple or returns an error tuple with a default error.

Converts various values to an OK tuple format.

Enhanced pipe operator with support for ellipsis (...) placement.

Nil-coalescing "or" operator.

OK-pipe operator.

Functions

Link to this macro

l <~> r

(macro)

Error-coalescing operator.

Similar to the nil-coalescing operator (|||), but applies a similar logic of the OK-pipe (~>).

It return the right side value if the left side is:

  • nil
  • :error
  • {:error, _}

Examples

iex> nil <~> "default"
"default"

iex> :error <~> "default"
"default"

iex> {:error, :reason} <~> "default"
"default"

iex> {:ok, "value"} <~> "default"
{:ok, "value"}

iex> "value" <~> "default"
"value"

iex> false <~> "default"
false

Extracts values from OK tuples.

  • {:ok, value} returns value
  • {:error, _} returns nil
  • :error returns nil
  • Any other value is returned unchanged

Examples

iex> from_ok({:ok, 123})
123

iex> from_ok({:error, :reason})
nil

iex> from_ok(:error)
nil

iex> from_ok(123)
123

Wraps a value in an OK tuple if it's not already in a result tuple format.

  • {:ok, value}, {:error, reason} and :error are returned unchanged
  • Any other value x is converted to {:ok, x}

Examples

iex> ok({:ok, 123})
{:ok, 123}

iex> ok({:error, :reason})
{:error, :reason}

iex> ok(:error)
:error

iex> ok(123)
{:ok, 123}

Wraps a value in an OK tuple or returns an error tuple with a default error.

  • {:ok, value}, {:error, reason} and :error are returned unchanged
  • nil returns {:error, err} where err is the default error provided in the second argument
  • Any other value x returns {:ok, x}

Examples

iex> ok_or({:ok, 123}, :default_error)
{:ok, 123}

iex> ok_or({:error, :reason}, :default_error)
{:error, :reason}

iex> ok_or(:error, :default_error)
:error

iex> ok_or(nil, :default_error)
{:error, :default_error}

iex> ok_or(123, :default_error)
{:ok, 123}

Converts various values to an OK tuple format.

  • {:ok, value} and {:error, reason} are returned unchanged
  • :error is returned unchanged
  • nil is converted to :error
  • Any other value x is converted to {:ok, x}

Examples

iex> to_ok({:ok, 123})
{:ok, 123}

iex> to_ok({:error, :reason})
{:error, :reason}

iex> to_ok(:error)
:error

iex> to_ok(nil)
:error

iex> to_ok(123)
{:ok, 123}
Link to this macro

l |> r

(macro)

Enhanced pipe operator with support for ellipsis (...) placement.

This is a more flexible drop-in replacement for the standard Elixir pipe operator (|>).

Special Features

  • The ellipsis (...) will be replaced with the result of evaluating the left-hand side expression.
  • The right-hand side need not be a function; it can be any expression containing the ellipsis (...).
  • You may use the ellipsis multiple times, and the left-hand side will be calculated exactly once.
  • If no ellipsis is present, it behaves like the standard pipe operator (placing the value as the first argument).

Examples

# Standard first position pipe
iex> 2 |> Integer.to_string()
"2"

# Using ellipsis for explicit placement
iex> 2 |> Integer.to_string(...)
"2"

# Using ellipsis to place the piped value in a non-first position
iex> 3 |> String.pad_leading("2", ..., "0")
"002"

# Using the ellipsis multiple times
iex> 2 |> Kernel.==(..., ...)
true

# Nested expressions with ellipsis
iex> 2 |> String.pad_leading(Integer.to_string(...), 3, "0")
"002"

# With expressions and transformations
iex> 2 |> (... * 3)
6
Link to this macro

l ||| r

(macro)

Nil-coalescing "or" operator.

Works like the logical OR (||), except it only defaults to the right side if the left side is nil (whereas || also defaults on false and other falsy values).

Examples

iex> nil ||| "default"
"default"

iex> false ||| "default"
false

iex> 0 ||| "default"
0

iex> "" ||| "default"
""
Link to this macro

l ~> r

(macro)

OK-pipe operator.

Similar to the enhanced pipe (|>), but with additional error handling for the following patterns:

  • {:ok, value} - Extracts value and passes it to the right side
  • {:error, _} - Passes through unchanged (short-circuits the pipeline)
  • :error - Passes through unchanged (short-circuits the pipeline)
  • nil - Passes through unchanged (short-circuits the pipeline)
  • Any other value - Passes the value directly to the right side

Examples

iex> 2 ~> Integer.to_string()
"2"

iex> {:ok, 2} ~> Integer.to_string() |> String.pad_leading(3, "0")
"002"

iex> {:error, :reason} ~> Integer.to_string()
{:error, :reason}

# Note that this would pass :error to `String.pad_leading` breaking our pipe: 
# :error ~> Integer.to_string() |> String.pad_leading(2, "0")
:error
# Instead we want to do:
iex> :error ~> Integer.to_string() ~> String.pad_leading(2, "0")
:error

iex> nil ~> Integer.to_string()
nil

iex> 2 ~> (... * 2)
4

# With a non-standard position using ellipsis
iex> 2 ~> Kernel./(3, ...)
1.5