A couple weeks ago, we have pushed support to unquote fragments:
Enum.each [a: 1, b: 2], fn { k, v } ->
def map(unquote(k)), do: unquote(v)
end
Although it provide a nice way to dynamically define functions, after discussing with developers and extending Elixir to rely more on it, some flaws became aparent.
The whole idea of unquote fragments is that you can only use it when calling another macro. This starts leaking the abstraction because the main purpose of having the same invocation syntax for functions and macros in Elixir is to not have to make a distinction in between them in the first place!
Furthermore, it is only valid in a macro when it expects a quoted expression. For example, if you want to dynamically define tests for the function above, this approach won't work:
Enum.each [a: 1, b: 2], fn { k, v } ->
test "map with value #{unquote(k)}" do
assert map(unquote(k)) == unquote(v)
end
end
This is because the only quoted expression is inside the do...end
block. The test name is a simple value and we can't use unquote
on it.
Besides adding semantics on the library user side, the library developer needs to explicitly whitelist and allow the use of unquote fragments via the Macro.escape_quoted
function. This puts more burden on macro developers, since it is one extra step they need to be aware of when writing proper macros.
The biggest problem we wanted to solve with unquote fragments was to make the defsequence
implementation in IO.ANSI
easier. However, we can't still use it because of quote/unquote nesting. Let's see a simpler example:
# Compiles a sum
defmacro sum(left, right) do
quote do
total = unquote(left) + unquote(total)
def add(unquote(left), unquote(right)), do: unquote(total)
end
end
Notice that in the example above, the def
wants to unquote contents coming from the sum
macro but also unquote the total
variable defined inside the quote. This doesn't work, because the unquote
to total will apply upfront.
One solution is to add a escape
special macro (other lisps provide a similar mechanism), rewriting it as:
defmacro sum(left, right) do
quote do
total = unquote(left) + unquote(total)
def add(unquote(left), unquote(right)), do: escape(unquote(total))
end
end
Even though, I believe this solution to not be worthy it. The current approach is still limited and semantically overloaded. We need to look for more solutions.
One of the solutions discussed previously is to rely more on @attributes
and make the following syntax available:
Enum.each [a: 1, b: 2], fn { k, v } ->
@k k
@v v
def map(@k), do: @v
end
The problem with this approach though is that we need to explicitly define the variables @k
and @v
and then use them. One proposal is to allow @attributes
to behave more like variables and allow them to be explicitly assigned:
@k = 1
@k #=> 1
If you want to explicitly match against its value, you need to write:
^@k = 1
This would allow us to write the following:
Enum.each [a: 1, b: 2], fn { @k, @v } ->
def map(^@k), do: @v
end
Notice we need to use ^@k
inside map
because we want to match against that value.
This approach solves the problems with the current solution, although it is slightly backwards incompatible. One other issue with this approach is that it raises the chances of conflicts. In this case, we are using @attributes
as variables raising the chance of it conflicting with an actual attribute like @compile
, for example, one would write:
Enum.each [a: 1, b: 2], fn { @k, @compile } ->
def map(^@k), do: @compile
end
This wouldn't work, since @compile
is an registered attribute. One final issue is that it still requires some effort of the macro developer. A custom macro that is doing work at compilation time wouldn't work because it would receive a call to @k
instead of the real attribute:
# What the macro below receives is not the value
# in @k but the representation { :@, [], { :k, [], 13 } }
@k 13
some_macro(@k)
Another solution to this problem is to be able to define a quote that is injected directly into the current tree. The example above would be rewriten as:
Enum.each [a: 1, b: 2], fn { k, v } ->
quote_inject do
def map(unquote(k)), do: unquote(v)
end
end
Note: other suggestions for names are: inject_quoted
, inline_quote
, quote_inline
, what else?
This is a nice approach because it makes explicit that we are inside a quote, so the developer is aware the same rules would apply. The same options available in quote
would be available here too. It would also work under any scenario. The only downside is that it adds an extra nesting level when you have to use it.
So, what do you think?
My current verdict is: "better is the enemy of good"