Skip to content

Instantly share code, notes, and snippets.

@christhekeele
Last active April 17, 2018 22:19
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 christhekeele/32ddbbffea07dc1013c0ef71d73aaa41 to your computer and use it in GitHub Desktop.
Save christhekeele/32ddbbffea07dc1013c0ef71d73aaa41 to your computer and use it in GitHub Desktop.
Possible macro API for generating match patterns/specifications in Elixir
defmodule Matcha do
@moduledoc """
Documentation for Matcha.
"""
@doc """
Handles the sigil `~m`.
It returns a match pattern or specification.
## Modifiers
* `p`: generates a simple match pattern (default)
* `b`: generates a match spec that returns all bound values
* `s`: generates a match spec that returns the entire match target
## Examples
iex> import Matcha
iex> ~m({1, two, :three})
{1, :"$0", :three}
iex> ~m({1, two, :three})p
{1, :"$0", :three}
iex> import Matcha
iex> ms = ~m({1, two, :three})b
[{{1, :"$0", :three}, [], [%{two: :"$0"}]}]
iex> :ets.test_ms({1, 2, :three}, ms)
{:ok, %{two: 2}}
iex> import Matcha
iex> ms = ~m({1, two, :three})s
[{{1, :"$0", :three}, [], [:"$_"]}]
iex> :ets.test_ms({1, 2, :three}, ms)
{:ok, {1, 2, :three}}
"""
defmacro sigil_m(match, modifiers)
defmacro sigil_m({:<<>>, _meta, [string]}, flags) when is_binary(string) do
string
|> build_match(__CALLER__, flags)
|> Macro.escape
end
defp build_match(string, caller, []) do
build_match(string, caller, 'p')
end
defp build_match(pattern, caller, 'p') do
pattern
|> Code.string_to_quoted!
|> do_pattern(caller)
|> elem(0)
end
defp build_match(head, caller, 'b') do
head
|> Code.string_to_quoted!
|> do_head(caller)
|> do_bindings(caller)
|> do_unassignment(caller)
|> List.wrap
end
defp build_match(head, caller, 's') do
head
|> Code.string_to_quoted!
|> do_head(caller)
|> do_target(caller)
|> do_unassignment(caller)
|> List.wrap
end
defp build_match(_string, _caller, _flags) do
raise ArgumentError, "modifier flag must be one of: p, b, s"
end
@doc """
Generates more complicated matchspecs.
## Examples
iex> import Matcha
iex> ms = match do { x, y } when x - y > 0 -> x + y end
[{{:"$0", :"$1"}, [{:>, {:-, :"$0", :"$1"}, 0}], [{:+, :"$0", :"$1"}]}]
iex> :ets.test_ms({2, -1}, ms)
{:ok, 1}
iex> import Matcha
iex> ms = match do
{ x, y } -> x + y
{ x, y, z } -> x + y + z
end
[
{{:"$0", :"$1"}, [], [{:+, :"$0", :"$1"}]},
{{:"$0", :"$1", :"$2"}, [], [{:+, {:+, :"$0", :"$1"}, :"$2"}]}
]
iex> :ets.test_ms({2, 1}, ms)
{:ok, 3}
iex> :ets.test_ms({2, 1, 200}, ms)
{:ok, 203}
"""
defmacro match([do: clauses]) do
for {:"->", _, [[head], body]} <- clauses do
head
|> do_head(__CALLER__)
|> do_body(body, __CALLER__)
|> do_unassignment(__CALLER__)
end |> Macro.escape
end
defp do_pattern(pattern, caller) do
pattern
|> Macro.expand(%{caller | context: :match})
|> Macro.traverse(%{}, fn
{ref, meta, context} = node, vars
when is_atom(ref) and is_list(meta) and is_atom(context) ->
{node, Map.put_new(vars, ref, :"$#{map_size(vars)}")}
node, vars ->
{node, vars}
end, &do_rewrite/2)
end
defp do_head(head, caller) do
{pattern, guards} = :elixir_utils.extract_guards(head)
{pattern, vars} = do_pattern(pattern, caller)
guards = do_guards(guards, vars, caller)
{pattern, guards, vars}
end
defp do_body({pattern, guards, vars}, body, caller) do
{body, caller} = :elixir_expand.expand(body, %{caller |
context: nil,
vars: vars |> Map.keys |> Enum.map(&({&1, nil}))
})
{pattern, guards, body
|> Macro.postwalk(vars, &do_rewrite/2)
|> elem(0)
|> List.wrap
}
end
defp do_guards(guards, vars, caller) do
for guard <- guards do
do_guard(guard, vars, caller)
end
end
defp do_guard(guard, vars, caller) do
{guard, caller} = :elixir_expand.expand(guard, %{caller |
context: :guard,
vars: vars |> Map.keys |> Enum.map(&({&1, nil}))
})
guard
|> Macro.postwalk(vars, &do_rewrite/2)
|> elem(0)
end
defp do_bindings({pattern, guards, vars}, _caller) do
[{pattern, guards, [vars]}]
end
defp do_target({pattern, guards, vars}, _caller) do
[{pattern, guards, [:"$_"]}]
end
defp do_rewrite({ref, meta, context}, vars) when is_atom(ref) and is_list(meta) and is_atom(context) do
{Map.fetch!(vars, ref), vars}
end
defp do_rewrite({:".", _, [:erlang, op]}, vars) do
{op, vars}
end
defp do_rewrite({:"{}", _, args}, vars) do
{List.to_tuple(args), vars}
end
defp do_rewrite({:"[]", _, args}, vars) do
{args, vars}
end
defp do_rewrite({op, _, args}, vars) do
{List.to_tuple([op | args]), vars}
end
defp do_rewrite(node, vars) do
{node, vars}
end
defp do_unassignment({{:=, left, right}, guards, [body]}, caller) when left == body do
{right, guards, [:"$_"]}
end
defp do_unassignment({{:=, left, right}, guards, [body]}, caller) when right == body do
{left, guards, [:"$_"]}
end
defp do_unassignment(spec, _caller) do
spec
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment