This is another proposal to solve the issue described in #846.
To recap, imagine we have a keywords list and we want to define functions from it:
[foo: 1, bar: 2, bat: 3]
Imagine each function receives an argument and adds the argument to the list number:
Enum.each list, fn { key, value } ->
def unquote(key)(arg) do
unquote(value) + arg
end
end
The more complex example described in #846 would be written as follows:
Enum.each [bright: 1, faint: 2], fn { name, code } ->
def unquote(name), do: "\e[#{unquote(code)}m"
defp escape_sequence(<< unquote(atom_to_binary(name)), rest :: binary >>), do:
{ "\e[#{unquote(code)}m", rest }
end
It is clearly shorter than the proposal described in #846 and it simply builds on top of the familiar unquote and unquote_splicing mechanisms.
The basic idea of the mechanism is very simple. Elixir already has two function definitions formats. The first of them is def/2:
def name(args) when guard, do: body
The second is the low level def/4:
def quoted_name, quoted_args, quoted_guards, do: quoted_body
Now, this proposal would be the equivalent to implementing def/2 as the following macro:
defmacro def(name_and_args_and_guard, do: body) do
quote do
mag = quote do: unquote(name_and_args_and_guard)
{ name, args, guards } = Macro.split_name_and_args_and_guards(mag)
body = quote do: unquote(body)
def name, args, guards, do: body
end
end
This means that the definition we saw above:
def unquote(key)(arg) do
unquote(value) + arg
end
Would literally translate to:
mag = quote do: unquote(key)(arg)
{ name, args, guards } = Macro.split_name_and_args_and_guards(mag)
body = quote do: unquote(value) + arg
def name, args, guards, do: body
Notice how the unquote(key)(arg) we used as the function name becomes part of a quote when the def/2 macro is expanded. This is all this proposal is about. Since both arguments given to def/2 will be passed to a quote, we can use unquote on them, making meta programming easier. As long as the macro is structured similarly, we can use those techniques. For example defdelegate could be improved to work in the same way, allowing one to do:
Enum.each [:foo, :bar, :baz], fn(x) ->
defdelegate unquote(x)(arg1, arg2), to: :some_module
end
One of the "downsides" of this proposal is that it needs to contain a small backwards incompatible change but I believe it is a good change. Today, if you have a nested quote, all the nested unquotes apply to the first quote. For example:
x = 1
Macro.to_binary(quote do: (quote do: unquote(x)))
#=> quote() do
1
end
Notice how the unquote was expanded even if it was inside another quote. This is actually bad because having a quote inside another quote becomes much less useful.
After this proposal, unquote will only work outside of nested quotes:
x = 1
Macro.to_binary(quote do: (quote do: unquote(x)))
#=> quote() do
unquote(x)
end
As you can see, this is a breaking change. When the code is executed, there is no x in that context. Luckily, it is very easy to fix it:
Macro.to_binary(quote do
x = unquote(x)
quote do: unquote(x)
end
In other words, by changing this behaviour we will actually make nested quotes more useful, because we can better control how each expression is unquoted.
Due to the nature of how @attributes work in Elixir, they have been used as constants in Elixir code. By accepting this proposal, using unquote would be a viable option too. Imagine this:
max_attempts = 10
def do_something(attempts) when attempts < unquote(max_attempts), do: ...
In case we consider this approach to be the preferred approach, then #846 has no merit and should be closed. In particular, if we continue using @attributes as constants, it includes the chances of having name conflicts (imagine someone by mistake names a constant @behaviour). Using variables, as above, those cannot happen.
This proposes intends to make definition of dynamic functions easier by formalizing the definition of def/2, defp/2 and friends to be part of quoted expressions. The only downside is a backwards incompatible change that seems to be welcome regardless of this proposal.
I like this... especially the nested unquote part — had to work around this limitation so many times. Hope this change doesn't really break anything, though.
This has been merged into master.
For others who (like me) stumble on this and want more context, this feature is called "unquote fragments". It's discussed in the docs and was also discussed on the mailing list at the time of this issue. Here's the actual commit.
Most helpful comment
For others who (like me) stumble on this and want more context, this feature is called "unquote fragments". It's discussed in the docs and was also discussed on the mailing list at the time of this issue. Here's the actual commit.