Can someone enlighten me on this piece of code?

This is a code snippet from the metaprogramming elixir book.

defmacro test(description, do: test_block) do
    test_func = String.to_atom(description)
    quote do
      @tests {unquote(test_func), unquote(description)}
      def unquote(test_func)(), do: unquote(test_block)
    end
  end

Can someone enlighten me on what is def unquote(test_func)(), do: unquote(test_block) doing? How is it even legal syntax??? I tried it out on iex:

iex(67)> defmodule Test do
...(67)> def test(a)(), do: a
...(67)> end
** (CompileError) iex:68: invalid syntax in def test(a)()
    iex:68: (module)

unquote is a function to “create code from data” simply said.

When this is executed
def unquote(test_func)(), do: unquote(test_block)

the result will be
def :description_as_atom(), do: [list, of, test, blocks]
which will be valid code for the compiler.

Short: unquote will mostly be called in macros to generate code from parameters (which is data at compile time), so only the result of unquote will stay afterwards.

3 Likes

@laiboonh: Inside quote do_block your expressions are quoted.
For example:

defmodule Example do
  defmacro sample do
    IO.inspect quote do: a
    :ok
  end
end
require Example
Example.sample
# {:a, [], Example}

Similarly test_func and description are quoted too.
Simple explanation for new developer is that unquote converts quoted expressions (AST) to their “normal” form.
For example:

defmodule Example do
  defmacro sample(a) do
    quote do
      unquote(a)
    end
  end
end
require Example
Example.sample(5)
# returns 5 instead of: {:a, [], Example}

So your line is “translated” from Elixir AST:

def {:test_func, [], ModuleName}(), do: {:block, [], ModuleName}

to “normal” code like:

def test_func(), do: test_block

You can see how it looks by inspecting variables in quote do_block without unquote part.
Please see: Quote and unquote for more informations.

2 Likes

Hi, thanks for helping me out here. One thing i don’t understand is what actually happens in unquote. I have modified your code slightly

defmodule Example do
  defmacro sample(a) do
    IO.inspect quote do: unquote(a)
    :ok
  end
end

Let’s not use AST literals but something else so that we can see the difference. Example.sample(%{name: "Lai"}). This is my understanding, correct me if i’m wrong.

I understand that arguments become quoted expression inside the body of a macro. Hence a becomes {:%{}, [line: 41], [name: "Lai"]} inside the macro body.

unquote is like string interpolation hence we “substitute” in a and the macro body becomes IO.inspect quote do: {:%{}, [line: 41], [name: "Lai"]}

finally when you quote some quoted expression (AST) you get back the same quoted expression and IO.inspect prints out {:%{}, [line: 41], [name: "Lai"]}

Is this how it goes? I am a bit confused by your statement.

  1. Was there any conversion going on or simply a substitution like i said.
  2. What is “normal” form? is it from my example {:%{}, [line: 41], [name: "Lai"]} or %{name: "Lai"}

Think of unquote as similar to #{} in strings. If you have name = "Bob"; greeting = "hello #{name}" the contents of name are injected into the greeting string producing "hello Bob".

When you do something like

capitalize_ast = quote do: String.capitalize("hello")
result = quote do: IO.inspect(unquote(capitalize_ast))

you’re inserting the AST contents of capitalize_ast into result which makes result the AST: IO.inspect(unquote(String.capitalize("hello"))

2 Likes

Actually i was just curious whether unquote does a substitution or does a conversion as well like Eiji mentioned. I rationalized that its like you said a mere substitution.

iex> x= quote do: %{name: "Lai"}
iex> (quote do: IO.inspect(unquote(x))) |> Macro.to_string
"IO.inspect(%{name: \"Lai\"})"

iex> x = %{name: "Lai"}
%{name: "Lai"}
iex(130)> (quote do: IO.inspect(unquote(x))) |> Macro.to_string
"IO.inspect(%{name: \"Lai\"})"

No matter unquote is unquoting an expression or a quoted expression, it will in the end result in an AST because unquote has to happen within a quote block

@laiboonh: You can think about AST like a Assembler code.
AST is a data (in tuple notation), but it could be nested instead of simple Assembler plain instructions list.
quote is changing code to AST, so you are generating quoted code in quote block. You cannot access variables outside of this block unless you call unquote.
You can think about quoting like about generating file using controller data. Your controller data are all variables inside your macro/function including it’s arguments. To access them instead of calling <%= assigns.something %> you are using unquote(something).
unquote fetches raw value of variable and/or expression and puts it into quote block.
Finally quote will return quoted code block and Elixir compiler will generate “normal” code (normal - I mean that code you can see - without AST) - it’s expanding instructions to real code.

About your code:

  1. You are unquoting quoted expression (here expression is just a map).
  2. You are unquoting raw expression (again - here it’s just map).

both of them returns unquoted raw value of that expression.

What unquote is doing is like undo of one quote call and evaluate it putting into quoted expression.
When you are working for example with lists in recursive method like:

def do_something([head | tail]), do: to_string(head) <> do_something(tail)

you need to implement also case when do_something is called with empty list like:

def do_something([]), do: ""

Similarly unquote works with raw values. You have quoted variable/expression n times. unquote is doing undo for first (top) quote, but when it’s called with raw value then it just returns that value and finally in both cases it evaluates it.
So:

first = quote do: unquote 5
second = quote do: unquote quote do: 5
first == second # true
# but:
third = quote do: unquote quote do: quote do: 5
first != third # true
# more:
fourth = quote do: 5
first == fourth
fifth = quote do: quote do: 5
third == fifth

Note: I used here raw value (5) that’s already unquoted.

Eh, not really like assembler, it is the very definition of AST. Elixir’s AST. :slight_smile:

The compilation process goes: Elixir → Elixir’s AST (what quote gives you) → Erlang (Abstract Format, basically it’s AST) → Core Erlang → BEAM
With a variety of translators along the way too. :slight_smile:

Basically take this Elixir:

defmodule :tester do

  def hi, do: "there"

end

To this Elixir AST:

{:defmodule, [context: Elixir, import: Kernel],
 [:tester,
  [do: {:def, [context: Elixir, import: Kernel],
    [{:hi, [context: Elixir], Elixir}, [do: "there"]]}]]}

To this Erlang:

-module(tester).
-export([hi/0]).
hi() -> <<"there">>.

To this Core Erlang:

module 'tester' ['hi'/0,
                 'module_info'/0,
                 'module_info'/1]
    attributes []
'hi'/0 =
    %% Line 3
    fun () ->
        #{#<116>(8,1,'integer',['unsigned'|['big']]),
          #<104>(8,1,'integer',['unsigned'|['big']]),
          #<101>(8,1,'integer',['unsigned'|['big']]),
          #<114>(8,1,'integer',['unsigned'|['big']]),
          #<101>(8,1,'integer',['unsigned'|['big']])}#
'module_info'/0 =
    fun () ->
        call 'erlang':'get_module_info'
            ('tester')
'module_info'/1 =
    fun (_cor0) ->
        call 'erlang':'get_module_info'
            ('tester', _cor0)

To this BEAM Assembly:

00007F031584F908: i_func_info_IaaI 0 tester hi 0
00007F031584F930: move_return_c <<"there">>

00007F031584F940: i_func_info_IaaI 0 tester module_info 0
00007F031584F968: move_cr tester r(0)
00007F031584F978: allocate_tt 0 1
00007F031584F988: call_bif_e erlang:get_module_info/1
00007F031584F998: deallocate_return_Q 0

00007F031584F9A8: i_func_info_IaaI 0 tester module_info 1
00007F031584F9D0: move_rx r(0) x(1)
00007F031584F9E0: move_cr tester r(0)
00007F031584F9F0: allocate_tt 0 2
00007F031584FA00: call_bif_e erlang:get_module_info/2
00007F031584FA10: deallocate_return_Q 0

And that is what is loaded by the VM.

3 Likes

Nice code there to challenge one’s understanding of unquote but i think its more illustrative if we used something that is not an AST Literal

#From the inside out, quote do: {1,2,3} returns {:{}, [], [1, 2, 3]}, unquote just interpolates 
#this inside the AST (outer quote block) hence we end up with {:{}, [], [1, 2, 3]}
iex(13)> quote do: unquote quote do: {1,2,3}
{:{}, [], [1, 2, 3]}

#we interpolate {1,2,3} into the AST (quote block), this is an invalid quoted expression 
#by the way
iex(14)> quote do: unquote {1,2,3}
{1, 2, 3}

#we quote on a quote. quote is also a function call hence we end with with something like this
iex(15)> quote do: unquote quote do: quote do: {1,2,3}
{:quote, [], [[do: {:{}, [], [1, 2, 3]}]]}

#quote on an expression to get a quoted expression (of that expression)
iex(16)> quote do: {1,2,3}
{:{}, [], [1, 2, 3]}

#again we do a quote on quote
iex(17)> quote do: quote do: {1,2,3}
{:quote, [], [[do: {:{}, [], [1, 2, 3]}]]}