defmodule Croma.Result do
@moduledoc """
A simple data structure to represent a result of computation that can either succeed or fail,
in the form of `{:ok, any}` or `{:error, any}`.
In addition to many utility functions, this module also provides implementation of
`Croma.Monad` interface for `t:Croma.Result.t/1`.
This enables the following Haskell-ish syntax:
iex> use Croma
...> Croma.Result.m do
...> x <- {:ok, 1}
...> y <- {:ok, 2}
...> pure x + y
...> end
{:ok, 3}
The above code is expanded to the code that uses `pure/1` and `bind/2`.
Croma.Result.bind({:ok, 1}, fn x ->
Croma.Result.bind({:ok, 2}, fn y ->
Croma.Result.pure(x + y)
end)
end)
This is useful when handling multiple computations that may go wrong in a short-circuit manner:
iex> use Croma
...> Croma.Result.m do
...> x <- {:error, :foo}
...> y <- {:ok, 2}
...> pure x + y
...> end
{:error, :foo}
"""
use Croma.Monad
import Croma.Defun
@type t(a, b) :: {:ok, a} | {:error, b}
@type t(a) :: t(a, any)
@type t :: t(any)
@doc """
Simply checks if the given term is ok- or error-tuple.
Using this function you can write e.g.
`r :: v[Croma.Result.t(integer)]`
in your parameter list of `defun` macro to validate `r` is of type `t:t/0`.
However note that this function only checks the outmost structure of an argument;
2nd value in the 2-tuple won't be validated for the given type parameter
(in the above example it won't verify whether `r` contains an `integer` or not).
"""
defun valid?(t :: any) :: boolean do
{:ok , _} -> true
{:error, _} -> true
_ -> false
end
@doc """
Implementation of `pure` operation of Monad (or Applicative).
Wraps the given value into a `Croma.Result`, i.e., returns `{:ok, arg}`.
"""
def pure(a), do: {:ok, a}
@doc """
Implementation of `bind` operation of Monad.
Executes the given function if the result is in `:ok` state; otherwise returns the failed result.
"""
def bind({:ok, val} , f), do: f.(val)
def bind({:error, _} = result, _), do: result
# Override default implementation to make it tail-recursive
def sequence(l) do
sequence_impl(l, [])
end
defunp sequence_impl(l :: [t(a)], acc :: [a]) :: t([a]) when a: any do
([] , acc) -> {:ok, Enum.reverse(acc)}
([h | t], acc) ->
case h do
{:ok , v} -> sequence_impl(t, [v | acc])
{:error, _} = e -> e
end
end
@doc """
Returns the value associated with `:ok` in the given result.
Returns `nil` if the result is in the form of `{:error, _}`.
## Examples
iex> Croma.Result.get({:ok, 1})
1
iex> Croma.Result.get({:error, :foo})
nil
"""
defun get(result :: t(a)) :: nil | a when a: any do
{:ok , val} -> val
{:error, _ } -> nil
end
@doc """
Returns the value associated with `:ok` in the given result.
Returns `default` if the result is in the form of `{:error, _}`.
## Examples
iex> Croma.Result.get({:ok, 1}, 0)
1
iex> Croma.Result.get({:error, :foo}, 0)
0
"""
defun get(result :: t(a), default :: a) :: a when a: any do
({:ok , val}, _ ) -> val
({:error, _ }, default) -> default
end
@doc """
Returns the value associated with `:ok` in the given result.
Raises `ArgumentError` if the result is in the form of `{:error, _}`.
## Examples
iex> Croma.Result.get!({:ok, 1})
1
iex> Croma.Result.get!({:error, :foo})
** (ArgumentError) element not present: {:error, :foo}
"""
defun get!(result :: t(a)) :: a when a: any do
{:ok , val} -> val
{:error, _ } = e -> raise ArgumentError, message: "element not present: #{inspect(e)}"
end
@doc """
Returns true if the given result is in the form of `{:ok, _value}`.
"""
defun ok?(result :: t(a)) :: boolean when a: any do
{:ok , _} -> true
{:error, _} -> false
end
@doc """
Returns true if the given result is in the form of `{:error, _}`.
"""
defun error?(result :: t(a)) :: boolean when a: any do
!ok?(result)
end
@doc """
Executes the given function within a try-rescue block and wraps the return value as `{:ok, retval}`.
If the function raises an exception, `try/1` returns the exception in the form of `{:error, exception}`.
## Examples
iex> Croma.Result.try(fn -> 1 + 1 end)
{:ok, 2}
iex> Croma.Result.try(fn -> raise "foo" end)
{:error, %RuntimeError{message: "foo"}}
"""
defun try(f :: (-> a)) :: t(a) when a: any do
try do
{:ok, f.()}
rescue
e -> {:error, {e, [:try]}}
end
end
@doc """
Tries to take one result in `:ok` state from the given two.
If the first result is in `:ok` state it is returned.
Otherwise the second result is returned.
Note that `or_else/2` is a macro instead of a function in order to short-circuit evaluation of the second argument,
i.e. the second argument is evaluated only when the first argument is in `:error` state.
"""
defmacro or_else(result1, result2) do
quote do
case unquote(result1) do
{:ok , _} = r1 -> r1
{:error, _} -> unquote(result2)
end
end
end
@doc """
Transforms a result by applying a function to its contained `:error` value.
If the given result is in `:ok` state it is returned without using the given function.
"""
defun map_error(result :: t(a), f :: ((any) -> any)) :: t(a) when a: any do
case result do
{:error, e} -> {:error, f.(e)}
{:ok, _} = r -> r
end
end
@doc """
Wraps a given value in an `:ok` tuple if `mod.valid?/1` returns true for the value.
Otherwise returns an `:error` tuple.
"""
defun wrap_if_valid(v :: a, mod :: module) :: t(a) when a: any do
case mod.valid?(v) do
true -> {:ok, v}
false -> {:error, {:invalid_value, [mod]}}
end
end
@doc """
Based on existing functions that return `Croma.Result.t(any)`, defines functions that raise on error.
Each generated function simply calls the specified function and then passes the returned value to `Croma.Result.get!/1`.
## Examples
iex> defmodule M do
...> def f(a) do
...> {:ok, a + 1}
...> end
...> Croma.Result.define_bang_version_of(f: 1)
...> end
iex> M.f(1)
{:ok, 2}
iex> M.f!(1)
2
If appropriate spec of original function is available, spec of the bang version is also declared.
For functions that have default arguments it's necessary to explicitly pass all arities to `Croma.Result.define_bang_version_of/1`.
"""
defmacro define_bang_version_of(name_arity_pairs) do
quote bind_quoted: [name_arity_pairs: name_arity_pairs, caller: Macro.escape(__CALLER__)] do
specs = Croma.TypeUtil.fetch_spec_info_at_compile_time(__MODULE__)
Enum.each(name_arity_pairs, fn {name, arity} ->
spec = Enum.find_value(specs, &Croma.Result.Impl.match_and_convert_spec(name, arity, &1, caller))
if spec do
@spec unquote(spec)
end
vars = Croma.Result.Impl.make_vars(arity, __MODULE__)
def unquote(:"#{name}!")(unquote_splicing(vars)) do
unquote(name)(unquote_splicing(vars)) |> Croma.Result.get!()
end
end)
end
end
defmodule Impl do
@moduledoc false
def match_and_convert_spec(name, arity, spec, caller_env) do
case spec do
{:::, meta1, [{^name, meta2, args}, ret_type]} when length(args) == arity ->
convert(name, meta1, meta2, args, ret_type, caller_env)
_ ->
nil
end
end
defp convert(name, meta1, meta2, args, ret_type, caller_env) do
make_spec_fun = fn r -> {:::, meta1, [{:"#{name}!", meta2, args}, r]} end
case ret_type do
{:ok, r} -> make_spec_fun.(r)
{:|, _, types} ->
Enum.find_value(types, fn
{:ok, r} -> make_spec_fun.(r)
_ -> nil
end)
{{:., _, [mod_alias, :t]}, _, r} ->
if Macro.expand(mod_alias, caller_env) == Croma.Result, do: make_spec_fun.(hd(r)), else: nil
_ -> nil
end
end
def make_vars(n, module) do
if n == 0 do
[]
else
Enum.map(0 .. n-1, fn i -> Macro.var(String.to_atom("arg#{i}"), module) end)
end
end
end
defmodule ErrorReason do
@moduledoc false
@type context :: module | {module, atom}
defun add_context(reason :: term, context :: context) :: {term, [context]} do
({reason, contexts}, context) -> {reason, [context | contexts]}
(term , context) -> {term , [context ]}
end
end
end