Packages

Result-chaining operators for Elixir

Current section

Files

Jump to
result_chain lib result_chain.ex
Raw

lib/result_chain.ex

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2026 Dmitriy Pertsev
defmodule ResultChain do
@moduledoc """
Result-aware chaining operator for Elixir workflows.
`ResultChain` adds the `~>` operator for chaining steps that may return
success or error values. Successful results continue to the next step, while
errors stop the chain and are returned unchanged.
`~>` treats values as follows:
- `{:ok, value}` unwraps to `value` before calling the next step
- `:ok` continues as the value `:ok`
- `:error` short-circuits the chain and returns `:error`
- `{:error, reason}` short-circuits the chain and returns `{:error, reason}`
- `nil` is treated as a successful value
- any other non-error value is treated as a successful value
There are two ways to integrate the operator:
- `use ResultChain` imports `ResultChain` and locally replaces `Kernel.|>/2`,
so `|>` and `~>` can be mixed in one chain
- `import ResultChain` imports only this module's macros, which is useful for
`~>`-only chains
## Mixed chains with `use ResultChain`
```elixir
defmodule MyWorkflow do
use ResultChain
def parse(value) do
case Integer.parse(value) do
{integer, _} -> integer
_ -> :error
end
end
def reciprocal(0), do: {:error, :division_by_zero}
def reciprocal(value), do: {:ok, 1 / value}
def run(value) do
value
|> parse()
~> reciprocal()
end
end
```
## `~>`-only chains with `import ResultChain`
```elixir
defmodule MyWorkflow do
import ResultChain
def parse(value) do
case Integer.parse(value) do
{integer, _} -> {:ok, integer}
_ -> {:error, :not_an_integer}
end
end
def reciprocal(0), do: {:error, :division_by_zero}
def reciprocal(value), do: {:ok, 1 / value}
def run(value) do
value
~> parse()
~> reciprocal()
end
end
```
"""
import ResultChain.ChainBuilder
@chain_operators [:~>, :|>]
defmacro __using__(_opts) do
quote do
import Kernel, except: [|>: 2]
import ResultChain, warn: false
end
end
defmacro is_chain_operator(op) do
quote do
unquote(op) in unquote(@chain_operators)
end
end
defmacro unquote(:|>)(left, right) do
ast = {:|>, [], [left, right]}
chain = unpipe(ast)
build_chain(chain)
end
@doc """
Chains a value or result into the next call with result-aware short-circuiting.
The right-hand side receives the unwrapped successful value from the left-hand
side. If the left-hand side evaluates to `:error` or `{:error, reason}`, the
remaining steps are skipped and that error is returned unchanged.
Success values are:
- `:ok`
- `{:ok, value}`
- `nil`
- any other non-error value
Failure values are:
- `:error`
- `{:error, reason}`
## Examples
Successful chaining:
```elixir
iex> import ResultChain
iex> parse = fn value ->
...> case Integer.parse(value) do
...> {integer, _} -> {:ok, integer}
...> _ -> {:error, :not_an_integer}
...> end
...> end
iex> reciprocal = fn
...> 0 -> {:error, :division_by_zero}
...> value -> {:ok, 1 / value}
...> end
iex> {:ok, "10"} ~> parse.() ~> reciprocal.()
{:ok, 0.1}
```
Short-circuiting on error:
```elixir
iex> import ResultChain
iex> reciprocal = fn value -> {:ok, 1 / value} end
iex> {:error, :not_an_integer} ~> reciprocal.()
{:error, :not_an_integer}
```
"""
defmacro left ~> right do
ast = {:~>, [], [left, right]}
chain = unpipe(ast)
build_chain(chain)
end
def code_from_ast(ast) do
Macro.to_string(Macro.expand_once(ast, __ENV__))
end
defp unpipe(expr) do
:lists.reverse(unpipe(expr, []))
end
defp unpipe({op, _, [left, right]}, acc) when is_chain_operator(op) do
unpipe({op, right}, unpipe(left, acc))
end
defp unpipe({op, other}, acc) when is_chain_operator(op) do
[other, op | acc]
end
defp unpipe(other, acc) do
[other | acc]
end
end