defmodule Reather.Either do
@type ok(t) :: {:ok, t}
@type error(t) :: {:error, t}
@type either(t) :: ok(t) | error(any)
@type ok_like :: any
@type error_like :: any
@type either_like :: ok_like | error_like
@doc """
Convert a value into `ok` or `error` tuple. The result is a tuple having
an `:ok` or `:error` atom for the first element, and a value for the second
element.
## Examples
iex> Either.new(:ok)
{:ok, nil}
iex> Either.new(:error)
{:error, nil}
iex> Either.new({:ok, 3})
{:ok, 3}
iex> Either.new({:error, "error!"})
{:error, "error!"}
iex> Either.new({:ok, 1, 2})
{:ok, {1, 2}}
iex> Either.new({:error, "error", :invalid})
{:error, {"error", :invalid}}
iex> Either.new({1, 2})
{:ok, {1, 2}}
iex> Either.new({})
{:ok, {}}
iex> Either.new(1)
{:ok, 1}
"""
@spec new(any) :: either_like
def new(:ok), do: {:ok, nil}
def new(:error), do: {:error, nil}
def new({:ok, v}), do: {:ok, v}
def new({:error, v}), do: {:error, v}
def new(v) when is_tuple(v) and tuple_size(v) > 0 do
case elem(v, 0) do
result when result in [:ok, :error] ->
{result, Tuple.delete_at(v, 0)}
_ ->
{:ok, v}
end
end
def new(v), do: {:ok, v}
@doc """
Wrap a value with an ok tuple.
## Examples
iex> Either.ok(1)
{:ok, 1}
iex> Either.ok({:error, 1})
{:ok, {:error, 1}}
"""
@spec ok(t) :: ok(t) when t: any
def ok(v), do: {:ok, v}
@doc """
Wrap a value with an error tuple.
## Examples
iex> Either.error(1)
{:error, 1}
iex> Either.error({:ok, 1})
{:error, {:ok, 1}}
"""
@spec error(t) :: error(t) when t: any
def error(v), do: {:error, v}
@doc """
Unwrap a value from an ok tuple.
## Examples
iex> Either.unwrap({:ok, 1})
1
iex> Either.unwrap({:error, 1})
** (RuntimeError) 1
iex> Either.unwrap({:ok, 1, 2, 3})
{1, 2, 3}
"""
@spec unwrap(ok_like) :: any
def unwrap({:ok, v}), do: v
def unwrap({:error, v}), do: raise(RuntimeError, v |> inspect())
def unwrap(v), do: new(v) |> unwrap()
@doc """
Unwrap a value from an ok tuple.
If the value is an error tuple, use passed default value or function.
## Examples
iex> Either.unwrap_or({:ok, 1}, 0)
1
iex> Either.unwrap_or({:error, ""}, 0)
0
iex> Either.unwrap_or({:error, ""}, fn -> "default" end)
"default"
iex> Either.unwrap_or(:error, "hello")
"hello"
"""
@spec unwrap_or(either_like, t | (() -> t)) :: t when t: any
def unwrap_or({:ok, v}, _), do: v
def unwrap_or({:error, _}, f) when is_function(f), do: f.()
def unwrap_or({:error, _}, default), do: default
def unwrap_or(v, default), do: new(v) |> unwrap_or(default)
@doc """
Check if the value is an ok tuple.
## Examples
iex> Either.ok?({:ok, 1})
true
iex> Either.ok?(:ok)
true
iex> Either.ok?({:error, 1})
false
iex> Either.ok?(:error)
false
"""
@spec ok?(either_like) :: boolean
def ok?({:ok, _}), do: true
def ok?({:error, _}), do: false
def ok?(v), do: new(v) |> ok?()
@doc """
Create a either from a boolean.
## Examples
iex> Either.confirm(true)
{:ok, nil}
iex> Either.confirm(false, :value_error)
{:error, :value_error}
"""
@spec confirm(boolean, t) :: either(t) when t: any
def confirm(boolean, err \\ nil)
def confirm(true, _), do: {:ok, nil}
def confirm(false, err), do: {:error, err}
@doc """
Map a function to the either.
If the either is `ok`, the function is applied to the value.
If the either is `error`, it returns as is.
## Examples
iex> {:ok, 1} |> Either.map(fn x -> x + 1 end)
{:ok, 2}
iex> {:error, 1} |> Either.map(fn x -> x + 1 end)
{:error, 1}
iex> :ok |> Either.map(fn _ -> 1 end)
{:ok, 1}
"""
@spec map(either_like, (any -> t)) :: either(t) when t: any
def map({:ok, value}, fun) do
{:ok, fun.(value)}
end
def map({:error, err}, _) do
{:error, err}
end
def map(v, fun) do
new(v) |> map(fun)
end
@doc """
Map a function to the `error` tuple.
## Examples
iex> {:error, 1} |> Either.map_err(fn x -> x + 1 end)
{:error, 2}
iex> {:ok, 1} |> Either.map_err(fn x -> x + 1 end)
{:ok, 1}
iex> :error |> Either.map_err(fn _ -> 1 end)
{:error, 1}
"""
@spec map_err(either_like, (any -> t)) :: either(t) when t: any
def map_err({:ok, v}, _), do: {:ok, v}
def map_err({:error, v}, fun), do: {:error, fun.(v)}
def map_err(v, map), do: new(v) |> map_err(map)
@doc """
Transform a list of eithers to an either of a list.
If any of the eithers is `error`, the result is `error`.
## Examples
iex> [{:ok, 1}, {:ok, 2}] |> Either.traverse()
{:ok, [1, 2]}
iex> [{:ok, 1}, {:error, "error!"}, {:ok, 2}]
...> |> Either.traverse()
{:error, "error!"}
"""
@spec traverse([either_like]) :: either([any])
def traverse(traversable) when is_list(traversable) do
traversable
|> Enum.map(&new(&1))
|> Enum.reduce_while([], fn
{:ok, v}, acc -> {:cont, [v | acc]}
{:error, err}, _acc -> {:halt, {:error, err}}
end)
|> case do
{:error, _} = e -> e
vs -> {:ok, Enum.reverse(vs)}
end
end
end