defmodule Arrows do
@moduledoc "./README.md" |> File.stream!() |> Enum.drop(1) |> Enum.join()
defmacro __using__(_options) do
quote do
import Kernel, except: [|>: 2]
import unquote(__MODULE__),
only: [
|>: 2,
|||: 2,
~>: 2,
<~>: 2,
ok: 1,
to_ok: 1,
from_ok: 1
]
end
end
import Kernel, except: [|>: 2]
@doc """
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
"""
defmacro l |> r, do: pipe(:first, :normal, l, r)
@doc """
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
"""
defmacro l ~> r, do: pipe(:first, :ok, l, r)
@doc """
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"
""
"""
defmacro l ||| r, do: join(:normal, l, r)
@doc """
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
"""
defmacro l <~> r, do: join(:ok, l, r)
@doc """
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}
"""
def to_ok(x) do
case x do
{:ok, _} -> x
{:error, _} -> x
:error -> :error
nil -> :error
x -> {:ok, x}
end
end
@doc """
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
"""
def from_ok(x) do
case x do
{:ok, x} -> x
{:error, _} -> nil
:error -> nil
# lenience
x -> x
end
end
@doc """
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}
"""
def ok(x = {:ok, _}), do: x
def ok(x = {:error, _}), do: x
def ok(:error), do: :error
def ok(x), do: {:ok, x}
@doc """
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}
"""
def ok_or(x = {:ok, _}, _), do: x
def ok_or(x = {:error, _}, _), do: x
def ok_or(:error, _), do: :error
def ok_or(nil, err), do: {:error, err}
def ok_or(ok, _), do: {:ok, ok}
defp ellipsis(l, arg) do
Macro.prewalk(arg, 0, fn form, acc ->
case form do
{:..., _, ctx} when is_atom(ctx) or ctx == [] -> {l, acc + 1}
_ -> {form, acc}
end
end)
end
defp pipe_args(where, l, args) do
case ellipsis(l, args) do
{args, 0} when where == :first -> [l | args]
{args, _} -> args
end
end
defp pipe(where, kind, l, r) do
v = Macro.var(:ret, __MODULE__)
case r do
{name, meta, args} ->
args = if(is_list(args), do: args, else: [])
continue = {name, meta, pipe_args(where, v, args)}
case kind do
:normal ->
quote generated: true do
unquote(v) = unquote(l)
unquote(continue)
end
:ok ->
quote generated: true do
case unquote(l) do
nil -> nil
:error -> :error
{:error, _} = unquote(v) -> unquote(v)
{:ok, unquote(v)} -> unquote(continue)
unquote(v) -> unquote(continue)
end
end
end
_ ->
case ellipsis(l, r) do
{arg, 0} ->
raise RuntimeError,
message: "Can't pipe into #{inspect(r)}: missing ellipsis(`...`) in #{inspect(arg)}"
{continue, _} ->
case kind do
:normal ->
quote generated: true do
unquote(v) = unquote(l)
unquote(continue)
end
:ok ->
quote generated: true do
case unquote(l) do
nil -> nil
:error -> :error
{:error, _} = unquote(v) -> unquote(v)
{:ok, unquote(v)} -> unquote(continue)
unquote(v) -> unquote(continue)
end
end
end
end
end
end
defp join(kind, l, r) do
v = Macro.var(:l, __MODULE__)
case kind do
:normal ->
quote generated: true do
unquote(v) = unquote(l)
if is_nil(unquote(v)), do: unquote(r), else: unquote(v)
end
:ok ->
quote generated: true do
case unquote(l) do
nil -> unquote(r)
:error -> unquote(r)
{:error, _} -> unquote(r)
unquote(v) -> unquote(v)
end
end
end
end
end