Packages

Comfortable, Explicit, Multi-Layered and Well-Documented Specifications for all your configurations, settings and options

Current section

Files

Jump to
specify lib solution.ex
Raw

lib/solution.ex

defmodule Solution do
@moduledoc """
A Macro-based approach to working with ok/error tuples
"""
@doc """
Matches when `x` is one of the following:
- `:ok`
- `{:ok, _}`
- `{:ok, _, _}`
- or a longer tuple where the first element is the atom `:ok`
iex> is_ok(:ok)
true
iex> is_ok({:ok, 42})
true
iex> is_ok({:ok, "I", "have", "many", "elements"})
true
iex> is_ok(:asdf)
false
iex> is_ok({:error, "failure"})
false
"""
defguard is_ok(x) when x == :ok or (is_tuple(x) and tuple_size(x) >= 1 and elem(x, 0) == :ok)
@doc """
Matches when `x` is a long-enough ok-tuple that has more than `n_elems` elements.
"""
defguard is_ok(x, n_elems) when is_ok(x) and (n_elems == 0 or tuple_size(x) >= n_elems)
@doc """
Matches when `x` is one of the following:
- `:error`
- `:undefined`
- `{:error, _}`
- `{:error, _, _}`
- or a longer tuple where the first element is the atom `:error`
iex> is_error(:error)
true
iex> is_error({:error, 42})
true
iex> is_error({:error, "I", "have", "many", "elements"})
true
iex> is_error(:asdf)
false
iex> is_error({:ok, "success!"})
iex> is_error(:undefined)
true
"""
defguard is_error(x) when x == :error or (is_tuple(x) and tuple_size(x) >= 1 and elem(x, 0) == :error) or x == :undefined
@doc """
Matches when `x` is a long-enough ok-tuple that has more than `n_elems` elements.
"""
defguard is_error(x, n_elems) when is_error(x) and (n_elems == 0 or tuple_size(x) >= n_elems)
@doc """
Matches when either `is_ok(x)` or `is_error(x)` matches.
iex> is_okerror({:ok, "Yay!"})
true
iex> is_okerror({:error, "Nay"})
true
iex> is_okerror(false)
false
iex> is_okerror(:undefined)
true
iex> is_okerror({})
false
iex> is_okerror({:ok, "the", "quick", "brown", "fox"})
true
"""
defguard is_okerror(x) when is_ok(x) or is_error(x)
@doc """
Matches when `x` is a long-enough ok-tuple that has more than `n_elems` elements.
Warning: Will _not_ match plain `:ok`, `:error` or `:undefined`!
"""
defguard is_okerror(x, n_elems) when is_ok(x, n_elems) or is_error(x, n_elems)
@doc """
Matches any ok datatype.
(See also `is_ok/1`)
Has to be used inside the LHS of a `scase` or `swith` statement.
"""
defmacro ok() do
guard_env = Map.put(__ENV__, :context, :guard)
{:when, [],
[
{:latest_solution_match____, [], nil},
{:is_ok, [context: Elixir, import: Solution],
[{:latest_solution_match____, [], nil}]}
]}
|> Macro.prewalk(&Macro.expand(&1, guard_env))
end
# Expands to x when is_ok(x, min_length)
# Used internally by `expand_match`
defmacro __ok__(min_length) do
guard_env = Map.put(__ENV__, :context, :guard)
{:when, [],
[
{:latest_solution_match____, [], nil},
{:is_ok, [context: Elixir, import: Solution],
[{:latest_solution_match____, [], nil}, min_length]}
]}
|> Macro.prewalk(&Macro.expand(&1, guard_env))
end
@doc """
Matches `{:ok, res}` (as well as tuples with more elements). `res` is then bound.
(See also `is_ok/1`)
Has to be used inside the LHS of a `scase` or `swith` statement.
"""
defmacro ok(res) do
quote do
{:ok, unquote(res)}
end
end
@doc """
Matches any error datatype.
(See also `is_error/1`)
Has to be used inside the LHS of a `scase` or `swith` statement.
"""
defmacro error() do
guard_env = Map.put(__ENV__, :context, :guard)
{:when, [],
[
{:latest_solution_match____, [], nil},
{:is_error, [context: Elixir, import: Solution],
[{:latest_solution_match____, [], nil}]}
]}
|> Macro.prewalk(&Macro.expand(&1, guard_env))
end
@doc """
Matches `{:error, res}` (as well as tuples with more elements). `res` is then bound.
(See also `is_error/1`)
Has to be used inside the LHS of a `scase` or `swith` statement.
"""
defmacro error(res) do
quote do
{:error, unquote(res)}
end
end
# Expands to x when is_error(x, min_length)
# Used internally by `expand_match`
defmacro __error__(min_length) do
guard_env = Map.put(__ENV__, :context, :guard)
{:when, [],
[
{:latest_solution_match____, [], nil},
{:is_error, [context: Elixir, import: Solution],
[{:latest_solution_match____, [], nil}, min_length]}
]}
|> Macro.prewalk(&Macro.expand(&1, guard_env))
end
@doc """
Matches any ok/error datatype.
Has to be used inside the LHS of a `scase` or `swith` statement.
"""
defmacro okerror() do
guard_env = Map.put(__ENV__, :context, :guard)
{:when, [],
[
{:latest_solution_match____, [], nil},
{:is_okerror, [context: Elixir, import: Solution],
[{:latest_solution_match____, [], nil}]}
]}
|> Macro.prewalk(&Macro.expand(&1, guard_env))
end
@doc """
Matches any ok or error type. `tag` is then boudn to `:ok`, `:error` or `:undefined`.
(See also `is_okerror/1`)
Has to be used inside the LHS of a `scase` or `swith` statement.
"""
defmacro okerror(tag) do
quote do
{unquote(tag), _} when unquote(tag) in [:ok, :error]
end
end
@doc """
Matches `{:ok, res}`, `{:error, res}` (as well as tuples with more elements). `tag` and `res` are bound.
(See also `is_okerror/1`)
`tag` is bound to the value `:ok` or `:error` depending on the tuple.
`res` is bound to what the second element might be.
Has to be used inside the LHS of a `scase` or `swith` statement.
"""
defmacro okerror(tag, res) do
quote do
{unquote(tag), unquote(res)} when unquote(tag) in [:ok, :error]
end
end
# Expands to x when is_okerror(x, min_length)
# Used internally by `expand_match`
defmacro __okerror__(0) do
quote do
okerror()
end
end
defmacro __okerror__(min_length) do
guard_env = Map.put(__ENV__, :context, :guard)
{:when, [],
[
{:latest_solution_match____, [], nil},
{:is_okerror, [context: Elixir, import: Solution],
[{:latest_solution_match____, [], nil}, min_length]}
]}
|> Macro.prewalk(&Macro.expand(&1, guard_env))
end
@doc """
Works like a normal `case`-statement,
but will expand `ok()`, `error()` and `okerror()`macros to the left side of `->`.
iex> scase {:ok, 10} do
...> ok() -> "Yay!"
...> _ -> "Failure"
...> end
"Yay!"
You can also pass arguments to `ok()`, `error()` or `okerror()` which will then be bound and available
to be used inside the case expression:
iex> scase {:ok, "foo", 42} do
...> ok(res, extra) ->
...> "result: \#{res}, extra: \#{extra}"
...> _ -> "Failure"
...> end
"result: foo, extra: 42"
Note that for `ok()` and `error()`, the first argument will match the first element after the `:ok` or `:error` tag.
On the other hand, for `okerror()`, the first argument will match the tag `:ok` or `:error`.
"""
defmacro scase(input, conditions) do
guard_env = Map.put(__ENV__, :context, :guard)
{:case, [], [input, conditions]}
|> Macro.prewalk(fn node ->
case node do
{:->, meta, [[lhs], rhs]} ->
{lhs, rhs_list} = expand_match(lhs, [rhs])
rhs = {:__block__, [], rhs_list}
node = {:->, meta, [[lhs], rhs]}
Macro.expand(node, guard_env)
_ ->
Macro.expand(node, guard_env)
end
end)
end
defp expand_match({tag, meta, args}, rhs) when tag in [:ok, :error, :okerror] and is_list(args) do
var = Macro.var(:latest_solution_match____, nil)
args_amount = args_amount(args)
prefixes =
args
|> Enum.with_index
|> Enum.map(fn {arg, index} ->
full_index = index + elem_offset(tag)
quote do
unquote(arg) = elem(unquote(var), unquote(full_index))
end
end)
lhs = {:"__#{tag}__", meta, [max(args_amount + elem_offset(tag) - 1, 0)]}
{lhs, prefixes ++ rhs}
end
defp expand_match(other, rhs) do
{other, rhs}
end
defp args_amount(args) do
case Enum.count(args) do
0 ->
0
other ->
other + 1
end
end
defp elem_offset(:okerror), do: 0
defp elem_offset(_), do: 1
@doc """
Works like a normal `with`-statement,
but will expand `ok()`, `error()` and `okerror()` macros to the left side of `<-`.
iex> x = {:ok, 10}
iex> y = {:ok, 33}
iex> swith ok(res) <- x,
...> ok(res2) <- y do
...> "We have: \#{res} \#{res2}"
...> else
...> _ -> "Failure"
...> end
"We have: 10 33"
You can also pass arguments to `ok()`, `error()` or `okerror()` which will then be bound and available
to be used inside the rest of the `swith`-expression:
iex> x = {:ok, 10}
iex> y = {:error, 33}
iex> z = {:ok, %{a: 42}}
iex> swith ok(res) <- x,
...> error(res2) <- y,
...> okerror(tag, metamap) <- z,
...> %{a: val} = metamap do
...> "We have: \#{res} \#{res2} \#{tag} \#{val}"
...> else
...> _ -> "Failure"
...> end
"We have: 10 33 ok 42"
Note that for `ok()` and `error()`, the first argument will match the first element after the `:ok` or `:error` tag.
On the other hand, for `okerror()`, the first argument will match the tag `:ok` or `:error`.
"""
defmacro swith(statements, conditions)
defmacro swith(statement, conditions) do
do_swith([statement], conditions)
end
# Since `swith` is a normal macro bound to normal function rules,
# define it for all possible arities.
for arg_num <- (1..252) do
args = (0..arg_num) |> Enum.map(fn num -> Macro.var(:"statement#{num}", __MODULE__) end)
@doc false
defmacro swith(unquote_splicing(args), conditions) do
do_swith(unquote(args), conditions)
end
end
defp do_swith(statements, conditions) do
guard_env = Map.put(__ENV__, :context, :guard)
statements =
statements
|> Enum.flat_map(fn node ->
case node do
{:<-, meta, [lhs, rhs]} ->
{lhs, extra_statements} =
expand_match(lhs, [])
lhs = Macro.expand(lhs, guard_env)
node = {:<-, meta, [lhs, rhs]}
[node | extra_statements]
_ ->
[Macro.expand(node, guard_env)]
end
end)
quote do
with(unquote_splicing(statements), unquote(conditions))
end
end
end