Skip to content

Instantly share code, notes, and snippets.

@am-kantox
Last active January 8, 2018 10:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save am-kantox/17540ab90343c87e76071ed2b7f428a2 to your computer and use it in GitHub Desktop.
Save am-kantox/17540ab90343c87e76071ed2b7f428a2 to your computer and use it in GitHub Desktop.
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