Last active
January 8, 2018 10:18
-
-
Save am-kantox/17540ab90343c87e76071ed2b7f428a2 to your computer and use it in GitHub Desktop.
Explicitly matching structs of listed types in function clauses
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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