Clear definition of unquote fragments

Hi there,

After reading @ericmj response here (Using unquote outside of quote block) about “unquote fragments”, I still can’t completely grasp the concept.

I’ve read the documentation many times now, besides some other resources (including McCord’s Metaprogramming Elixir), and I can’t reach to a concrete definition of what an unquote fragment is.

I’ve read in some places that “unquote fragments” is the usage of unquote that lets us dynamically define functions and nested macros (https://dockyard.com/blog/2016/08/16/the-minumum-knowledge-you-need-to-start-metaprogramming-in-elixir). After reading all this, I think both unquote/1 calls on the documentation example are “unquote fragments”, since they will be injected on the caller context and won’t have an explicit quote block enclosing them. Am I right?:

    defmacro defkv(kv) do
      quote bind_quoted: [kv: kv] do
        Enum.each kv, fn {k, v} ->
          def unquote(k)(), do: unquote(v)
        end
      end
    end

And consider this example please:

defmodule Foo do
  alias Baz

  fun_definition = {:bar, [], [{:arg, [], nil}]}
  body = {:__block__, [], [{:*, [], [{:arg, [], nil}, {:arg, [], nil}]}]}
  function = :bar
  arity = 1

  def(unquote(fun_definition)) do
    mfa = Baz.mfa(__MODULE__, unquote(function), unquote(arity)) # returns "Module.function/arity"
    Baz.run(mfa, fn -> unquote(body) end)
  end
end

Here I think the unquote(fun_definition) is definitely an unquote fragment because is allowing us to dynamically define the function. But I am not so sure regarding the other unquote/1 calls for function, arity and body. Since they are contributing for the dynamic definition of the function body, are they also unquote fragments?

Thanks in advance,
And thank you all for the amazing community!
lejboua

P.S.: Given the different opinions and definitions I’ve read, IMO the documentation could be updated to include an explicit unquote fragment definition. This way those like me who are starting to look at Elixir would be able to clearly distinguish between a “normal” unquote inside an explicit quote block and an unquote fragment.

P.S.2: I know that we can use the unquote/1 calls inside the def/2 macro arguments because the def/2 arguments, like any macro, are being quoted for us.

Many thanks!

3 Likes

I think this post has a good summary: Using unquote outside of quote block

1 Like

Hi @kip, thanks for the reply. I’ve mentioned that post on my initial message. I understand why we can do it, it may seem at first glance there’s no quote block wrapping the unquote call, but there is one in fact due to the macro arguments being quoted for us.

What I didn’t understand and think it isn’t completely clear on the documentation is the concept of unquote fragment. Saying that it is what we use to dynamically define a function IMO gives room for interpretation.

1 Like

I’m also curious about this.

iex(1)> quote do: unquote(:foo)
:foo
iex(2)> quote do: foo()
{:foo, [], []}
iex(3)> quote do: unquote({:foo, [], []})
{:foo, [], []}
iex(4)> quote do: unquote(:foo)()
{:foo, [], []}
  1. Quoting the unquoted AST of an atom is just an atom in AST. Makes sense.
  2. Quoting a function call produces the AST for a function call. Makes sense.
  3. Quoting the unquoted AST for a function call produces the same function call AST. Makes sense.
  4. Quoting an unquoted atom followed by parentheses produces the AST for a function call. Hmm, is it some kind of special treatment? Or something to do with how the compiler works internally? It’s tricky because we can’t picture what an unquoted atom AST looks like, and :foo() is not valid elixir.

That’s how quote/2 works, it simply quotes terms without further digging in. Maybe the example with unquote: false would shed some light.

iex|🌢|1 ▶ quote unquote: false, do: unquote(:foo)
{:unquote, [], [:foo]}
iex|🌢|2 ▶ quote unquote: false, do: unquote(:foo)()
{{:unquote, [], [:foo]}, [], []}

So the former results in :foo and the latter results in the plain AST triple with :foo coming from unquote(:foo) and the rest coming from parentheses. It’s like

iex|🌢|3 ▶ quote do: :foo + 42
{:+, [context: Elixir, imports: [{1, Kernel}, {2, Kernel}]], [:foo, 42]}

where :+ is the top-level AST term; () become a top-level AST term in your last example, but parentheses are a special case and they are becoming {:foo, [] = _meta, [] = _args} instead of literally the same {:(), [] = _meta, [:foo]}. :() notation is not possible because args might be not empty. Technically, it might have been {:(), [], [:foo, []]}, but it would make is harder to walk through.

2 Likes

This unquote: false example is very useful, thank you! It’s makes it easy to see how it’s the parentheses define that it’s a function call, and how:

  • unquote(:foo)() becomes {{:unquote, [], [:foo]}, [], []}

As this thread is about unquote fragments, is this what an unquote fragment is? There’s no special treatment, they’re just unquote/1 / {:unquote, ..., ...} calls that’s are left in the AST for later evaluation in context.

When we do something like:

defmodule Dynamic do
  name = :hello
  value = "world!"
  def unquote(name)(), do: unquote(value)
end

This will produce “unquote fragments” for name and value in the AST:

{:def, [context: Dynamic, imports: [{1, Kernel}, {2, Kernel}]],
 [
   {{:unquote, [], [{:name, [], Dynamic}]}, [context: Dynamic], []},
   [do: {:unquote, [], [{:value, [], Dynamic}]}]
 ]}

Which are resolved in Dynamic’s context when the def macro is evaluated.

1 Like