Packages

Tiny Elixir library for working with raw, {:ok, _}, or {:error, _} values

Current section

Files

Jump to
loe lib loe.ex
Raw

lib/loe.ex

defmodule Loe do
@moduledoc """
Loe is a tiny Elixir library for chaining `{:ok, _}`/`{:error, _}` results and raw values with elegant macros and helpers.
It helps normalize, transform, and chain computations while handling both successful and error results in a consistent way.
### Functions
- `lift/2` — Applies a function to a value if it is raw or `{:ok, _}`. Leaves `{:error, _}` or `:error` unchanged. Always returns `{:ok, _}` or `{:error, _}`.
- `tfil/2` — The inverse of `lift/2`. Applies a function **only** if the input is `:error` or `{:error, _}`. Leaves successful values unchanged.
- `unwrap!/1` — Extracts the value from `{:ok, value}`. Raises if the value is `:error` or `{:error, _}`.
### Operators
- `~>>` — Infix form of `lift/2`, for chaining operations on success.
- `<~>` — Infix form of `tfil/2`, for transforming errors.
### Example
```
value
~>> validate()
~>> normalize()
<~> wrap_error()
```
"""
@typedoc "Input may be a raw value, `{:ok, value}`, `{:error, error}`, or `:error`."
@type input(a, e) :: a | {:ok, a} | {:error, e} | :error
@typedoc "Normalized result value: either `{:ok, value}` or `{:error, error}`."
@type result(b, e) :: {:ok, b} | {:error, e}
@doc """
Applies a function to a raw or boxed input and returns a normalized result.
## Accepted input:
- A raw value `a`
- A tuple `{:ok, a}`
- An error tuple `{:error, e}`
- A bare `:error`
## Function `fun` may return:
- A raw value `b`
- A tuple `{:ok, b}`
- A tuple `{:error, e}`
- A bare `:error`
## Result:
Always returns either `{:ok, b}` or `{:error, e}`.
## Examples
iex> Loe.lift(2, fn x -> x * 2 end)
{:ok, 4}
iex> Loe.lift({:ok, 2}, fn x -> {:ok, x * 3} end)
{:ok, 6}
iex> Loe.lift({:ok, 2}, fn x -> x * 2 end)
{:ok, 4}
iex> Loe.lift(2, fn _x -> :error end)
{:error, :error}
iex> Loe.lift(2, fn _x -> {:error, :bad} end)
{:error, :bad}
iex> Loe.lift(:error, fn _ -> 123 end)
{:error, :error}
iex> Loe.lift({:error, :bad}, fn _ -> 123 end)
{:error, :bad}
"""
@spec lift(input(a, e), (a -> b | result(b, e))) :: result(b, e)
when a: any, b: any, e: any
def lift({:error, reason}, _fun), do: {:error, reason}
def lift(:error, _fun), do: {:error, :error}
def lift({:ok, val}, fun), do: do_lift(fun, val)
def lift(val, fun), do: do_lift(fun, val)
defp do_lift(fun, val) do
case fun.(val) do
{:ok, _} = ok -> ok
{:error, _} = err -> err
:error -> {:error, :error}
other -> {:ok, other}
end
end
@doc """
Unwraps a result value or raises an error.
- If given `{:ok, value}`, returns `value`.
- If given `{:error, reason}`, raises `RuntimeError` with the reason.
- If given anything else, raises `ArgumentError`.
## Examples
iex> Loe.unwrap!({:ok, 42})
42
iex> Loe.unwrap!({:error, :fail})
** (RuntimeError) Loe.unwrap!/1 encountered error: :fail
iex> Loe.unwrap!(123)
** (ArgumentError) Loe.unwrap!/1 expected {:ok, _} or {:error, _}, got: 123
"""
@spec unwrap!(any) :: any | no_return()
def unwrap!({:ok, val}), do: val
def unwrap!({:error, exception}) when is_exception(exception) do
raise exception
end
def unwrap!({:error, reason}) do
raise RuntimeError, "Loe.unwrap!/1 encountered error: #{inspect(reason)}"
end
def unwrap!(other) do
raise ArgumentError,
"Loe.unwrap!/1 expected {:ok, _} or {:error, _}, got: #{inspect(other)}"
end
@doc """
Infix `lift`.
Allows chaining like `value ~>> fun()`.
## Examples
iex> import Loe
...> 5 ~>> (fn x -> x + 2 end).()
{:ok, 7}
iex> import Loe
...> {:ok, 10} ~>> (fn x -> {:ok, x * 2} end).()
{:ok, 20}
iex> import Loe
...> :error ~>> (fn x -> x + 1 end).()
{:error, :error}
"""
defmacro left ~>> {call, line, args} do
value = quote do: value
args = [value | args || []]
quote do
Loe.lift(
unquote(left),
fn unquote(value) -> unquote({call, line, args}) end
)
end
end
@doc """
Applies a function to a raw or boxed error input and returns a normalized result.
## Accepted input:
- A raw value `a`
- A tuple `{:ok, a}`
- An error tuple `{:error, e}`
- A bare `:error`
## Function `fun` may return:
- A raw value `b`
- A tuple `{:ok, b}`
- A tuple `{:error, e}`
- A bare `:error`
## Result:
Always returns either `{:ok, b}` or `{:error, e}`.
## Examples
iex> Loe.tfil(2, fn _x -> :ignore_and_wrap end)
{:ok, 2}
iex> Loe.tfil({:ok, 2}, fn _x -> :ignore end)
{:ok, 2}
iex> Loe.tfil(:error, fn x -> %{a: x, b: 300} end)
{:error, %{a: :error, b: 300}}
iex> Loe.tfil({:error, :bad}, fn x -> %{a: x, b: 300} end)
{:error, %{a: :bad, b: 300}}
iex> Loe.tfil(:error, fn _x -> {:error, :reason} end)
{:error, :reason}
iex> Loe.tfil({:error, :bad}, fn _x -> {:error, :too_bad} end)
{:error, :too_bad}
"""
@spec tfil(input(a, e), (e -> f | result(a, f))) :: result(a, f)
when a: any, e: any, f: any
def tfil({:error, reason}, fun), do: do_tfil(fun, reason)
def tfil(:error, fun), do: do_tfil(fun, :error)
def tfil({:ok, val}, _fun), do: {:ok, val}
def tfil(val, _fun), do: {:ok, val}
defp do_tfil(fun, val) do
case fun.(val) do
{:ok, _} = ok -> ok
{:error, _} = err -> err
:error -> {:error, :error}
other -> {:error, other}
end
end
@doc """
Infix `tfil`.
Allows chaining like `value <~> fun()`.
## Examples
iex> import Loe
...> 5 <~> (fn x -> x + 2 end).()
{:ok, 5}
iex> import Loe
...> {:error, 10} <~> (fn x -> x * 2 end).()
{:error, 20}
iex> import Loe
...> {:error, 10} <~> (fn x -> {:ok, x * 2} end).()
{:ok, 20}
iex> import Loe
...> :error <~> (fn _x -> {:error, :nice} end).()
{:error, :nice}
"""
defmacro left <~> {call, line, args} do
value = quote do: value
args = [value | args || []]
quote do
Loe.tfil(
unquote(left),
fn unquote(value) -> unquote({call, line, args}) end
)
end
end
end