Packages
croma
0.4.6
0.13.0
0.12.0
0.11.3
0.11.2
0.11.1
0.11.0
0.10.2
0.10.1
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.3
0.7.2
0.7.1
0.7.0
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.1
0.5.0
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.11
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Elixir macro utilities to make type-based programming easier
Current section
Files
Jump to
Current section
Files
lib/croma/result.ex
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}`.
This module provides implementation of `Croma.Monad` interface for `Croma.Result.t`.
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) :: {:ok, a} | {:error, any}
@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 `Croma.Result`.
Returns `nil` if the argument 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 `Croma.Result`.
Returns `default` if the argument 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 `Croma.Result`.
Raises `ArgumentError` if the argument 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 argument 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 argument 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 `Croma.Result` in `:ok` state from the given two.
If the first `Croma.Result` is in `:ok` state it is returned.
Otherwise the second `Croma.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 `Croma.Result` by applying a function to its contained `:error` value.
If the given `Croma.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 """
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] do
specs = Module.get_attribute(__MODULE__, :spec)
Enum.each(name_arity_pairs, fn {name, arity} ->
spec = Enum.find_value(specs, &Croma.Result.Impl.match_and_convert_spec(name, arity, &1))
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) do
case spec do
{:spec, {:::, meta1, [{^name, meta2, args}, ret_type]}, env} when length(args) == arity ->
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, env) == Croma.Result, do: make_spec_fun.(hd(r)), else: nil
_ -> nil
end
_ -> 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
defun add_context(reason :: term, context :: atom) :: {term, [atom]} do
({reason, contexts}, context) -> {reason, [context | contexts]}
(term , context) -> {term , [context ]}
end
end
end