laiboonh

laiboonh

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)

Showing Posts 1 to 8

h4cc

h4cc

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.

Eiji

Eiji

@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.

laiboonh

laiboonh OP

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"}
benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

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"))

laiboonh

laiboonh OP

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

Eiji

Eiji

@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.

OvermindDL1

OvermindDL1

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.

laiboonh

laiboonh OP

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]}]]}
— All posts loaded —

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
webofbits
Aludel - LLM Evaluation Workbench Aludel is an embeddable Phoenix LiveView dashboard for evaluating and comparing LLM prompts across mult...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews