Create a gist now

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Explicitly matching structs of listed types in function clauses
defmodule DryStructMatch do
defmacrop clause!(name, mod, fun) do
quote bind_quoted: [name: name, mod: mod, fun: fun] do
quote do
def unquote(name)(
unquote({:%, [], [{:__aliases__, [alias: false], [mod]}, {:%{}, [], []}]}) = struct
) do
# |> ...
result = struct
unquote(:"#{name}_callback")(unquote(fun), result)
end
end
end
end
@doc ~S"""
Usage:
use DryStructMatch, update: [Foo, Bar: &IO.inspect/1]
The above will be expanded into two `update` clauses, the former having
no callback, and the latter having a callback that spits the result
out to console before returning it (due to `&IO.inspect/1`.)
"""
defmacro __using__(opts) do
Enum.flat_map(opts, fn {name, mods} ->
mods =
case mods do
[_ | _] -> mods
[] -> raise "Empty handler lists make no sense"
mod -> [mod]
end
[
quote do
defp unquote(:"#{name}_callback")({mod, fun}, result), do: apply(mod, fun, [result])
defp unquote(:"#{name}_callback")({mod, fun, args}, result) when is_list(args),
do: apply(mod, fun, [result | args])
defp unquote(:"#{name}_callback")(fun, result) when is_function(fun, 1),
do: fun.(result)
defp unquote(:"#{name}_callback")(_, result), do: result
end
| Enum.map(mods, fn
{mods, fun} when is_list(mods) -> Enum.map(mods, &clause!(name, &1, fun))
{mod, fun} -> clause!(name, mod, fun)
mod -> clause!(name, mod, nil)
end)
] ++
[
{:defoverridable, [context: Elixir, import: Kernel], [[{name, 1}]]}
]
end)
end
end
defmodule(Foo, do: defstruct(foo: 42))
defmodule(Foo1, do: defstruct(foo: 42))
defmodule(Foo2, do: defstruct(foo: 42))
defmodule(Bar, do: defstruct(bar: 42))
defmodule(Baz, do: defstruct(baz: 42))
defmodule A do
use DryStructMatch,
update: [Foo, {Bar, &IO.inspect/1}, {[Foo1, Foo2], {IO, :inspect, [[label: "inplace"]]}}],
empty: Foo1
def empty(input), do: super(input) && IO.inspect(input, label: "overloaded")
end
defmodule Test do
def test do
# prints "explicit: %Foo{foo: 42}"
IO.inspect(A.update(%Foo{}), label: "explicit")
# prints "%Bar{bar: 42}" (callback)
A.update(%Bar{})
# prints "inplace: %Foo2{foo: 42}" (callback)
A.update(%Foo2{})
# prints "overloaded: %Foo1{foo: 42}"
A.empty(%Foo1{})
# raises `FunctionClauseError`
A.update(%Baz{})
end
end
Test.test()
#⇒ explicit: %Foo{foo: 42}
# %Bar{bar: 42}
# inplace: %Foo2{foo: 42}
# overloaded: %Foo1{foo: 42}
# ** (FunctionClauseError) no function clause matching in A.update/1
#
# The following arguments were given to A.update/1:
#
# # 1
# %Baz{baz: 42}
#
# aa.ex:67: A.update/1
# (elixir) lib/code.ex:678: Code.require_file/2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment