I want to mimic ES6’s destructing assignment using Elixir’s sigil.
What I want to achieve is
~m{foo bar} = %{foo: 1, bar: 2}
foo #=> 1
bar #=> 2
So far I have the following code
defmodule DestructingAssignment do
defmacro __using__(_) do
quote do: import unquote(__MODULE__)
end
defmacro sigil_m({:<<>>, _line, [string]}, []) do
spec = string
|> String.split
|> Stream.map(&String.to_atom/1)
|> Enum.map(&{&1, {&1, [], Elixir}}) #=> [foo: {:foo, [], Elixir}, bar: {:bar, [], Elixir}]
{:%{}, [], spec}
end
end
When I run ~m{foo bar} = %{foo: 1, bar: 2}, pattern matching succeeds. But when I try to read the variable foo, it gives me
warning: variable "foo" does not exist and is being expanded to "foo()", please use parentheses to remove the ambiguity or change the variable name
iex:9
** (CompileError) iex:9: undefined function foo/0
I guess this is due to the macro hygiene, but as you can see, the sigil implementation doesn’t use quote. How can I inject local variable to the macro’s caller’s scope?
According to Saša Jurić’s article about macros, the third part of variable AST represents where the quoting happens. If it’s nil, the identifier is not hygienic. Check out paragraph “Discovering the AST structure” of the article and the series for the detail.
I have no ideas how macros work, but this is very useful and I made a string version also:
defmacro sigil_d({:<<>>, _line, [string]}, []) do
spec =
string
|> String.split()
|> Stream.map(&String.to_atom/1) # how could we do without this?
|> Enum.map(&{to_string(&1), {&1, [], nil}})
{:%{}, [], spec}
end