Nested Quotes

Hey guys,

I’ve run into a stumbling block. I need to write a macro for creating boilerplate event objects. I have something that looks like this:

defmacro event(opts, do: block) when is_list(opts) do
  mod = opts[:for]
  quote do
    handler_contents = quote do
      def handle(data) do
        unquote(block)
      end
    end
    Module.create(Module.concat(unquote(mod), Handler), handler_contents, Macro.Env.location(__ENV__))
  end
end

The problem is, block isn’t seen, because it’s nested twice. However, I have to nest the contents call in order to use Module.create. Can someone please enlighten me to the correct way to do this? It’s confusing the hell out of me :slight_smile:

Thanks,
Lee

Anyone have any ideas?

Try to find a way to do it without nesting quotes or take a look at @josevalim’s suggestion from elixir issue #612.

Yes, I saw that comment, but can’t reason how it would help. If I add unquote: false, then the variables in the Module.create won’t get unquoted. Not quite sure how it can be done, since in every respect, the block exists outside of the content quotes and the content quotes need to be passed to the Module.create calls non-unquoted.

Bizarre!

The secret tip for simple macros is to do a little work inside the macro and inside the quoted expression as possible. So I would rewrite it to:

defmodule MyModule do
  defmacro event(opts, do: block) when is_list(opts) do
    quote do
      MyModule.__event__(unquote(opts), unquote(block), __ENV__)
    end
  end

  def __event__(opts, block, env) do
    contents = 
      quote do
        def handle(data) do
          unquote(block)
        end
      end

    Module.create(Module.concat(opts[:for], Handler), contents, Macro.Env.location(env))
  end
end

It is very likely the above will not work quite as you want. Some bugs you may ran into:

  • Bug #1 - the block is being evaluated at compilation time and not inside the handle function. When I did the first unquote(block), the block contents will be evaluated in the place event was called. To fix that, do MyModule.__event__(unquote(opts), unquote(Macro.escape(block)), __ENV__). Escaping means that when the block is evaluated, it will evaluate to its AST.

  • Bug #2 - I believe you want to access the variable data inside the user supplied block. If so, you need to write def handle(var!(data)) instead of def handle(data). The former injects the data variable in the user context, the second means the variable belongs only to the quoted expression.

Btw, if you are defining dozens of events, it is likely compilation will become slower and slower. It turns out defining that many modules dynamically is not that good idea, specially modules with a single function, regardless I would advise you to consider a better approach like defining def handle(unquote(opts[:for]), data) so you have multiple clauses of handle in a single module.

5 Likes

Thanks, that worked beautifully. I’m not so worried about compile time, as there aren’t that many places this is used. It’s simply to make the code more readable.

Thanks again.