defmodule Oxide do
@moduledoc false
end
defmodule Oxide.Result do
@moduledoc """
Helpers for working with result tuples, `{:ok, value}` and `{:error, reason}`.
Unless otherwise stated, functions raise `FunctionClauseError` when given an unexpected
non-result. `:ok`, `:error`, `{:ok, value1, value2}`, etc. are not considered results.
"""
@type t :: {:ok, any()} | {:error, any()}
@type t(v) :: {:ok, v} | {:error, any()}
@type t(v, e) :: {:ok, v} | {:error, e}
@doc ~S"""
Result pipe operator.
The result pipe operator `&&&/2` is a result-aware analogue to the pipe operator `|>`.
It allows chaining functions that return results, piping the inner value of `:ok` results
and short-circuiting the pipeline if any of the functions return an error. For example
with {:ok, x1} <- f1(x),
{:ok, x2} <- f2(x1) do
f3(x2)
end
can be written as
x |> f1() &&& f2() &&& f3()
More examples:
iex> {:ok, :foo} &&& Atom.to_string()
"foo"
iex> {:ok, 3} &&& then(fn x -> {:ok, x + 1} end) &&& List.wrap()
[4]
iex> {:ok, :foo} &&& Atom.to_string() |> String.capitalize()
"Foo"
iex> {:error, :oops} &&& Atom.to_string() |> String.capitalize()
{:error, :oops}
"""
defmacro left &&& right do
quote do
case unquote(left) do
{:ok, t} -> t |> unquote(right)
{:error, e} -> {:error, e}
end
end
end
@doc """
Returns true if the given value is a result tuple.
iex> Result.result?({:ok, 42})
true
iex> Result.result?({:error, :not_found})
true
iex> Result.result?(42)
false
iex> Result.result?({:ok, 42, 43})
false
iex> Result.result?(:ok)
false
"""
@spec result?(any()) :: boolean()
def result?(maybe_result)
def result?({:ok, _}), do: true
def result?({:error, _}), do: true
def result?(_), do: false
@doc """
Assert a result.
Returns the value unchanged if it is a result; raises `RuntimeError` otherwise.
iex> Result.assert_result!({:ok, 42})
{:ok, 42}
iex> Result.assert_result!({:error, :not_found})
{:error, :not_found}
iex> Result.assert_result!(42)
** (RuntimeError) Not a result
iex> Result.assert_result!({:ok, 42, 43})
** (RuntimeError) Not a result
iex> Result.assert_result!(:ok)
** (RuntimeError) Not a result
"""
@spec assert_result!(any()) :: t() | no_return()
def assert_result!(maybe_result)
def assert_result!({:ok, value}), do: {:ok, value}
def assert_result!({:error, reason}), do: {:error, reason}
def assert_result!(_), do: raise("Not a result")
@doc ~S"""
Return whether a result is ok.
iex> Result.ok?({:ok, 3})
true
iex> Result.ok?({:error, 3})
false
"""
@spec ok?(t()) :: boolean
def ok?(result)
def ok?({:ok, _}), do: true
def ok?({:error, _}), do: false
@doc ~S"""
Wrap a value in an ok result.
iex> 3 |> Result.ok()
{:ok, 3}
iex> Result.ok({:ok, 3})
{:ok, {:ok, 3}}
"""
@spec ok(v) :: {:ok, v} when v: var
def ok(t), do: {:ok, t}
@doc ~S"""
Return whether a result is an error.
iex> Result.error?({:ok, 3})
false
iex> Result.error?({:error, 3})
true
"""
@spec error?(t()) :: boolean
def error?(result)
def error?({:ok, _}), do: false
def error?({:error, _}), do: true
@doc ~S"""
Wrap a value in an error result.
iex> :some_error_reason |> Result.error()
{:error, :some_error_reason}
"""
@spec error(e) :: {:error, e} when e: var
def error(e), do: {:error, e}
@doc ~S"""
Return true if an enumerable of results are all `ok`.
iex> Result.all?([{:ok, 1}, {:ok, 2}])
true
iex> Result.all?([{:ok, 1}, {:error, 2}])
false
"""
@spec all?([t()]) :: boolean
def all?(results), do: Enum.all?(results, &ok?/1)
@doc ~S"""
Return true if any of an enumerable of results is `ok`.
iex> Result.any?([{:ok, 1}, {:error, 2}])
true
iex> Result.any?([{:error, 1}, {:error, 2}])
false
"""
@spec any?([t()]) :: boolean
def any?(results), do: Enum.any?(results, &ok?/1)
@doc ~S"""
Unwrap an `:ok` result, and raise an `:error` reason.
If an error reason is an exception, it is raised as-is; otherwise, a `RuntimeError`
is raised with the inspected error reason in the exception message.
iex> Result.unwrap!({:ok, :value})
:value
iex> Result.unwrap!({:error, %{code: 500}})
** (RuntimeError) Unwrapped an error: %{code: 500}
iex> Result.unwrap!({:error, ArgumentError.exception("oh no")})
** (ArgumentError) oh no
"""
@spec unwrap!(t(v)) :: v when v: var
def unwrap!(result)
def unwrap!({:ok, t}), do: t
def unwrap!({:error, e}) when is_exception(e), do: raise(e)
def unwrap!({:error, e}), do: raise("Unwrapped an error: #{inspect(e)}")
@doc ~S"""
Unwrap an `:ok` result, falling back to `default` for an `:error` result.
iex> Result.unwrap_or({:ok, :cake}, :icecream)
:cake
iex> Result.unwrap_or({:error, :peas}, :icecream)
:icecream
"""
@spec unwrap_or(t(v), w) :: v | w when v: var, w: var
def unwrap_or(result, default)
def unwrap_or({:ok, t}, _default), do: t
def unwrap_or({:error, _}, default), do: default
@doc ~S"""
Unwrap an `:ok` result, or else act on the error in some way.
iex> Result.unwrap_or_else({:ok, 0}, fn e -> e + 1000 end)
0
iex> Result.unwrap_or_else({:error, 0}, fn e -> e + 1000 end)
1000
"""
@spec unwrap_or_else(t(v, e), (e -> w)) :: v | w when v: var, w: var, e: var
def unwrap_or_else(result, f)
def unwrap_or_else({:ok, t}, _f), do: t
def unwrap_or_else({:error, e}, f), do: f.(e)
@spec unwrap_err!(t(any, e)) :: e when e: var
def unwrap_err!(result)
def unwrap_err!({:ok, _}), do: raise("called `Result.unwrap_err()` on an `:ok` value")
def unwrap_err!({:error, e}), do: e
# def expect_err
@doc ~S"""
Transform a result, mapping an ok value with `f` and leaving errors unchanged.
Similar to `and_then/2` and useful for transformations that don't return a result.
iex> {:ok, 3} |> Result.map(fn x -> x + 1 end)
{:ok, 4}
iex> {:error, :nan} |> Result.map(fn x -> x + 1 end)
{:error, :nan}
"""
@spec map(t(v, e), (v -> w)) :: t(w, e) when v: var, w: var, e: var
def map(result, f)
def map({:ok, t}, f), do: {:ok, f.(t)}
def map({:error, e}, _f), do: {:error, e}
@doc ~S"""
Return a result leaving ok values unchanged but transforming an error reason with `f`.
iex> Result.map_err({:ok, 3}, fn x -> x + 1 end)
{:ok, 3}
iex> Result.map_err({:error, :nan}, &:erlang.atom_to_binary/1)
{:error, "nan"}
"""
@spec map_err(t(v, e), (e -> f)) :: t(v, f) when v: var, e: var, f: var
def map_err(result, f)
def map_err({:ok, t}, _f), do: {:ok, t}
def map_err({:error, e}, f), do: {:error, f.(e)}
@doc ~S"""
Return an _unwrapped_ ok value transformed by f, or `default` if `result` is an error.
iex> Result.map_or({:ok, 3}, 0, fn x -> x + 1 end)
4
iex> Result.map_or({:error, :nan}, 0, fn x -> x + 1 end)
0
"""
@spec map_or(t(v, any()), w, (v -> x)) :: x | w when v: var, w: var, x: var
def map_or(result, default, f)
def map_or({:ok, t}, _default, f), do: f.(t)
def map_or({:error, _}, default, _f), do: default
@doc ~S"""
Transform an unwrapped ok value with `f`, or return an error unchanged.
Similar to `map/2` except the transformed value is returned unwrapped. Useful when passing
a result into another function that returns a result.
iex> {:ok, 3} |> Result.and_then(fn x -> x + 1 end)
4
iex> {:error, :nan} |> Result.and_then(fn x -> x + 1 end)
{:error, :nan}
"""
@spec and_then(t(v, e), (v -> w)) :: w | {:error, e} when v: var, w: var, e: var
def and_then(result, f)
def and_then({:ok, t}, f), do: f.(t)
def and_then({:error, e}, _f), do: {:error, e}
@doc ~S"""
Return an ok result unchanged, or transform an unwrapped error reason with `f`.
iex> Result.or_else({:ok, :xylophone}, fn err -> err + 1 end)
{:ok, :xylophone}
iex> Result.or_else({:error, 3}, fn err -> err + 1 end)
4
"""
@spec or_else(t(v, e), (e -> f)) :: {:ok, v} | f when v: var, e: var, f: var
def or_else(result, f)
def or_else({:ok, t}, _f), do: {:ok, t}
def or_else({:error, e}, f), do: f.(e)
@doc ~S"""
Convert a maybe-nil value to a result.
Maps `nil` to `{:error, reason}` and any non-`nil` value to `{:ok, value}`.
iex> %{"key" => "value"} |> Map.get("key") |> Result.err_if_nil(:notfound)
{:ok, "value"}
iex> %{"key" => "value"} |> Map.get("missing") |> Result.err_if_nil(:notfound)
{:error, :notfound}
"""
@spec err_if_nil(v | nil, e) :: t(v, e) when v: var, e: var
def err_if_nil(value, reason)
def err_if_nil(value, reason) when is_nil(value), do: {:error, reason}
def err_if_nil(value, _reason) when not is_nil(value), do: {:ok, value}
@doc ~S"""
Equivalent to `Kernel.tap/2` for ok results.
Calls `f` with the value of an `:ok` result, and returns the result unchanged.
iex> {:ok, 3} |> Result.tap_ok(&IO.inspect/1)
3
{:ok, 3}
iex> {:error, :oops} |> Result.tap_ok(&IO.inspect/1)
{:error, :oops}
"""
@spec tap_ok(t(v), (v -> any())) :: t(v) when v: var
def tap_ok(result, f)
def tap_ok({:ok, t}, f) do
f.(t)
{:ok, t}
end
def tap_ok({:error, e}, _f), do: {:error, e}
@doc ~S"""
Equivalent to `Kernel.tap/2` for error results.
Calls `f` with the reason of an `:error` result, and returns the result unchanged.
iex> {:ok, 3} |> Result.tap_err(&IO.inspect/1)
{:ok, 3}
iex> {:error, :oops} |> Result.tap_err(&IO.inspect/1)
:oops
{:error, :oops}
"""
@spec tap_err(t(v, e), (e -> any())) :: t(v, e) when v: var, e: var
def tap_err(result, f)
def tap_err({:ok, t}, _f), do: {:ok, t}
def tap_err({:error, e}, f) do
f.(e)
{:error, e}
end
@doc ~S"""
Collects a list of results into a single result.
If any of the results is an error, the first error is returned. Otherwise, a single
ok result is returned with a list of the result values.
iex> [{:ok, 1}, {:ok, 2}, {:ok, 3}] |> Result.collect()
{:ok, [1, 2, 3]}
iex> [{:ok, 1}, {:error, 2}, {:ok, 3}, {:error, 4}] |> Result.collect()
{:error, 2}
"""
@spec collect([t()]) :: t()
def collect(results) do
Enum.find(results, false, fn r -> error?(r) end) ||
results |> Enum.map(&unwrap!/1) |> ok()
end
end
defmodule Oxide.Result.Dangerous do
@moduledoc """
Result helpers you should use with extreme care, or better yet, not at all.
"""
@doc ~S"""
Dangerous result pipe operator.
The result pipe operator `~>/2` is defined identically to `Oxide.Result.&&&/2`, and in
many circumstances behaves the same:
iex> {:ok, :foo} ~> Atom.to_string()
"foo"
iex> {:ok, 3} ~> then(fn x -> {:ok, x + 1} end) ~> List.wrap()
[4]
iex> {:ok, :foo} ~> Atom.to_string() |> String.capitalize()
"Foo"
iex> {:error, :oops} ~> then(fn x -> {:ok, x + 1} end) ~> List.wrap()
{:error, :oops}
However, Elixir's operator precedence means that when `~>` is followed by `|>`,
an early error can pipe into later stages of the pipeline in a way that is
almost never what you want.
iex> {:error, :oops} &&& Atom.to_string() |> String.capitalize()
{:error, :oops}
iex> {:error, :oops} ~> Atom.to_string() |> String.capitalize() # String.capitalize({:error, :oops})
** (FunctionClauseError) no function clause matching in String.capitalize/2
"""
defmacro left ~> right do
quote do
case unquote(left) do
{:ok, t} -> t |> unquote(right)
{:error, e} -> {:error, e}
end
end
end
end