Skip to content

Instantly share code, notes, and snippets.

@josevalim
Created April 12, 2013 15:35
Show Gist options
  • Save josevalim/5372932 to your computer and use it in GitHub Desktop.
Save josevalim/5372932 to your computer and use it in GitHub Desktop.

Flaws in unquote fragments

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.

You have to know when to use it

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.

Additional semantics

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.

It doesn't solve the original problem

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.

Other solutions

@attributes

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)

Quote injection

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?

@yrashk
Copy link

yrashk commented Apr 12, 2013

My current verdict is: "better is the enemy of good"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment