defmodule Bunch do
@moduledoc """
A bunch of general-purpose helper and convenience functions.
"""
alias __MODULE__.Type
@doc """
Imports a bunch of Bunch macros: `withl/1`, `withl/2`, `~>/2`, `~>>/2`, `quote_expr/1`, `quote_expr/2`
"""
defmacro __using__(_args) do
quote do
import unquote(__MODULE__),
only: [
withl: 1,
withl: 2,
~>: 2,
~>>: 2,
quote_expr: 1,
quote_expr: 2
]
end
end
@compile {:inline, listify: 1, error_if_nil: 2}
@doc """
Extracts the key from a key-value tuple.
"""
@spec key({key, value}) :: key when key: any, value: any
def key({key, _value}), do: key
@doc """
Extracts the value from a key-value tuple.
"""
@spec value({key, value}) :: value when key: any, value: any
def value({_key, value}), do: value
@doc """
Creates a short reference.
"""
@spec make_short_ref() :: Bunch.ShortRef.t()
defdelegate make_short_ref, to: Bunch.ShortRef, as: :new
@doc """
Works like `quote/2`, but doesn't require a do/end block and options are passed
as the last argument.
Useful when quoting a single expression.
## Examples
iex> use Bunch
iex> quote_expr(String.t())
quote do String.t() end
iex> quote_expr(unquote(x) + 2, unquote: false)
quote unquote: false do unquote(x) + 2 end
## Nesting
Nesting calls to `quote` disables unquoting in the inner call, while placing
`quote_expr` in `quote` or another `quote_expr` does not:
iex> use Bunch
iex> quote do quote do unquote(:code) end end == quote do quote do :code end end
false
iex> quote do quote_expr(unquote(:code)) end == quote do quote_expr(:code) end
true
"""
defmacro quote_expr(code, opts \\ []) do
{:quote, [], [opts, [do: code]]}
end
@doc """
A labeled version of the `with/1` macro.
This macro works like `with/1`, but enforces user to mark corresponding `withl`
and `else` clauses with the same label (atom). If a `withl` clause does not
match, only the `else` clauses marked with the same label are matched against
the result.
iex> use #{inspect(__MODULE__)}
iex> names = %{1 => "Harold", 2 => "Małgorzata"}
iex> test = fn id ->
...> withl id: {int_id, _} <- Integer.parse(id),
...> name: {:ok, name} <- Map.fetch(names, int_id) do
...> {:ok, "The name is \#{name}"}
...> else
...> id: :error -> {:error, :invalid_id}
...> name: :error -> {:error, :name_not_found}
...> end
...> end
iex> test.("1")
{:ok, "The name is Harold"}
iex> test.("5")
{:error, :name_not_found}
iex> test.("something")
{:error, :invalid_id}
`withl` clauses using no `<-` operator are supported, but they also have to be
labeled due to Elixir syntax restrictions.
iex> use #{inspect(__MODULE__)}
iex> names = %{1 => "Harold", 2 => "Małgorzata"}
iex> test = fn id ->
...> withl id: {int_id, _} <- Integer.parse(id),
...> do: int_id = int_id + 1,
...> name: {:ok, name} <- Map.fetch(names, int_id) do
...> {:ok, "The name is \#{name}"}
...> else
...> id: :error -> {:error, :invalid_id}
...> name: :error -> {:error, :name_not_found}
...> end
...> end
iex> test.("0")
{:ok, "The name is Harold"}
All the `withl` clauses that use `<-` operator must have at least one corresponding
`else` clause.
iex> use #{inspect(__MODULE__)}
iex> try do
...> Code.compile_quoted(quote do
...> withl a: a when a > 0 <- 1,
...> b: b when b > 0 <- 2 do
...> {:ok, a + b}
...> else
...> a: _ -> :error
...> end
...> end)
...> rescue
...> e -> e.description
...> end
"Label `:b` not present in withl else clauses"
## Variable scoping
Because the labels are resolved in the compile time, they make it possible to
access results of already succeeded matches from `else` clauses. This may help
handling errors, like below:
iex> use #{inspect(__MODULE__)}
iex> names = %{1 => "Harold", 2 => "Małgorzata"}
iex> test = fn id ->
...> withl id: {int_id, _} <- Integer.parse(id),
...> do: int_id = int_id + 1,
...> name: {:ok, name} <- Map.fetch(names, int_id) do
...> {:ok, "The name is \#{name}"}
...> else
...> id: :error -> {:error, :invalid_id}
...> name: :error -> {:ok, "The name is Defaultius the \#{int_id}th"}
...> end
...> end
iex> test.("0")
{:ok, "The name is Harold"}
iex> test.("5")
{:ok, "The name is Defaultius the 6th"}
## Duplicate labels
`withl` supports marking multiple `withl` clauses with the same label, however
in that case all the `else` clauses marked with such label are simply put multiple
times into the generated code. Note that this may lead to confusion, in particular
when variables are rebound in `withl` clauses:
iex> use #{inspect(__MODULE__)}
iex> test = fn x ->
...> withl a: x when x > 1 <- x,
...> do: x = x + 1,
...> a: x when x < 4 <- x do
...> :ok
...> else
...> a: x -> {:error, x}
...> end
...> end
iex> test.(2)
:ok
iex> test.(1)
{:error, 1}
iex> test.(3)
{:error, 4}
"""
@spec withl(keyword(with_clause :: term), do: code_block :: term(), else: match_clauses :: term) ::
term
defmacro withl(with_clauses, do: block, else: else_clauses) do
do_withl(with_clauses, block, else_clauses, __CALLER__)
end
@doc """
Works like `withl/2`, but allows shorter syntax.
## Examples
iex> use #{inspect(__MODULE__)}
iex> x = 1
iex> y = 2
iex> withl a: true <- x > 0,
...> b: false <- y |> rem(2) == 0,
...> do: {x, y},
...> else: (a: false -> {:error, :x}; b: true -> {:error, :y})
{:error, :y}
For more details and more verbose and readable syntax, check docs for `withl/2`.
"""
@spec withl(
keyword :: [
{key :: atom(), with_clause :: term}
| {:do, code_block :: term}
| {:else, match_clauses :: term}
]
) :: term
defmacro withl(keyword) do
{{:else, else_clauses}, keyword} = keyword |> List.pop_at(-1)
{{:do, block}, keyword} = keyword |> List.pop_at(-1)
with_clauses = keyword
do_withl(with_clauses, block, else_clauses, __CALLER__)
end
defp do_withl(with_clauses, block, else_clauses, caller) do
else_clauses =
else_clauses
|> Enum.map(fn {:->, meta, [[[{label, left}]], right]} ->
{label, {:->, meta, [[left], right]}}
end)
|> Enum.group_by(fn {k, _v} -> k end, fn {_k, v} -> v end)
with_clauses
|> Enum.reverse()
|> Enum.reduce(block, fn
{label, {:<-, meta, _args} = clause}, acc ->
label_else_clauses =
else_clauses[label] ||
"Label `#{inspect(label)}` not present in withl else clauses"
|> raise_compile_error(caller, meta)
{:with, meta, [clause, [do: acc, else: label_else_clauses]]}
{_label, clause}, acc ->
quote do
unquote(clause)
unquote(acc)
end
end)
end
@doc """
Embeds the argument in a one-element list if it is not a list itself. Otherwise
works as identity.
Works similarly to `List.wrap/1`, but treats `nil` as any non-list value,
instead of returning empty list in this case.
## Examples
iex> #{inspect(__MODULE__)}.listify(:a)
[:a]
iex> #{inspect(__MODULE__)}.listify([:a, :b, :c])
[:a, :b, :c]
iex> #{inspect(__MODULE__)}.listify(nil)
[nil]
"""
@spec listify(l) :: l when l: list
@spec listify(a) :: [a] when a: any
def listify(list) when is_list(list) do
list
end
def listify(non_list) do
[non_list]
end
@doc """
Returns an `:error` tuple if given value is `nil` and `:ok` tuple otherwise.
## Examples
iex> map = %{:answer => 42}
iex> #{inspect(__MODULE__)}.error_if_nil(map[:answer], :reason)
{:ok, 42}
iex> #{inspect(__MODULE__)}.error_if_nil(map[:invalid], :reason)
{:error, :reason}
"""
@spec error_if_nil(value, reason) :: Type.try_t(value)
when value: any(), reason: any()
def error_if_nil(nil, reason), do: {:error, reason}
def error_if_nil(v, _), do: {:ok, v}
@doc """
Returns given stateful try value along with its status.
"""
@spec stateful_try_with_status(result) :: {status, result}
when status: Type.try_t(),
result:
Type.stateful_try_t(state :: any) | Type.stateful_try_t(value :: any, state :: any)
def stateful_try_with_status({:ok, _state} = res), do: {:ok, res}
def stateful_try_with_status({{:ok, _res}, _state} = res), do: {:ok, res}
def stateful_try_with_status({{:error, reason}, _state} = res), do: {{:error, reason}, res}
@doc """
Helper for writing pipeline-like syntax. Maps given value using match clauses
or lambda-like syntax.
## Examples
iex> use #{inspect(__MODULE__)}
iex> {:ok, 10} ~> ({:ok, x} -> x)
10
iex> 5 ~> &1 + 2
7
Lambda-like expressions are not converted to lambdas under the hood, but
result of `expr` is injected to `&1` at the compile time.
Useful especially when dealing with a pipeline of operations (made up e.g.
with pipe (`|>`) operator) some of which are hard to express in such form:
iex> use #{inspect(__MODULE__)}
iex> ["Joe", "truck", "jacket"]
...> |> Enum.map(&String.downcase/1)
...> |> Enum.filter(& &1 |> String.starts_with?("j"))
...> ~> ["Words:" | &1]
...> |> Enum.join("\\n")
"Words:
joe
jacket"
"""
# Case when the mapper is a list of match clauses
defmacro expr ~> ([{:->, _, _} | _] = mapper) do
quote do
case unquote(expr) do
unquote(mapper)
end
end
end
# Case when the mapper is a piece of lambda-like code
defmacro expr ~> mapper do
{mapped, arg_present?} =
mapper
|> Macro.prewalk(false, fn
{:&, _meta, [1]}, _acc ->
quote do: {expr_result, true}
{:&, _meta, [i]} = node, acc when is_integer(i) ->
{node, acc}
{:&, meta, _}, _acc ->
"""
The `&` (capture) operator is not allowed in lambda-like version of \
`#{inspect(__MODULE__)}.~>/2`. Use `&1` alone instead.
"""
|> raise_compile_error(__CALLER__, meta)
other, acc ->
{other, acc}
end)
if not arg_present? do
"""
`#{inspect(__MODULE__)}.~>/2` operator requires either match clauses or \
at least one occurrence of `&1` argument on the right hand side.
"""
|> raise_compile_error(__CALLER__)
end
quote do
expr_result = unquote(expr)
unquote(mapped)
end
end
@doc """
Works similar to `~>/2`, but accepts only `->` clauses and appends default
identity clause at the end.
## Examples
iex> use #{inspect(__MODULE__)}
iex> {:ok, 10} ~>> ({:ok, x} -> {:ok, x+1})
{:ok, 11}
iex> :error ~>> ({:ok, x} -> {:ok, x+1})
:error
"""
defmacro expr ~>> ([{:->, _, _} | _] = mapper_clauses) do
default =
quote do
default_result -> default_result
end
quote do
case unquote(expr) do
unquote(mapper_clauses ++ default)
end
end
end
defmacro _expr ~>> _ do
"""
`#{inspect(__MODULE__)}.~>>/2` operator expects match clauses on the right \
hand side.
"""
|> raise_compile_error(__CALLER__)
end
defp raise_compile_error(reason, caller, meta \\ []) do
raise CompileError,
file: caller.file,
line: meta |> Keyword.get(:line, caller.line),
description: reason
end
end