Calling quote() with :bind_quoted option

According to the quote docs:

Options


:bind_quoted - passes a binding to the macro. Whenever a binding is given, unquote/1 is automatically disabled.

This code produces the expected output:

defmodule My do
  defmacro go(name) do
    quote bind_quoted: [name: name] do
      def unquote(name)() do
        unquote(name)
      end
    end
  end
end

defmodule Test do
  require My

  Enum.each [:hello, :goodbye], &My.go(&1)
end

IO.puts Test.hello
IO.puts Test.goodbye

Output:

~/elixir_programs$ iex macros2.ex
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]

hello
goodbye

Interactive Elixir (1.6.6) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)>

But, according to the quote docs, because a binding was given, unquote() is disabled, so shouldn’t I be able to remove all the calls to unquote()? This doesn’t work:

defmodule My do
  defmacro go(name) do
    quote bind_quoted: [name: name] do
      def :"#{name}"() do   # line 4
        name
      end
    end
  end
end

defmodule Test do
  require My

  Enum.each [:hello, :goodbye], &My.go(&1)
end

IO.puts Test.hello
IO.puts Test.goodbye

Output:

elixir_programs$ iex macros2.ex 
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]

** (SyntaxError) macros2.ex:4: syntax error before: '('
    (elixir) lib/code.ex:677: Code.require_file/2

And, I’m not even sure why the first example works because I’m using a value name rather than the key :name for the keyword list [name: name].

Even if I change the code to the following it still doesn’t work:

defmodule My do
  defmacro go(name) do
    quote bind_quoted: [name: name] do
      def hello() do
        name
      end
    end
  end
end

defmodule Test do
  require My

  Enum.each [:hello], &My.go(&1)
end

IO.puts Test.hello

The quote docs make no sense. :flushed:

2 Likes

This is because you’re crossing two “unquote” boundries in your macro. The first one is outside quote -> inside quote, which is handled by bind_quoted resulting in the following:

name = unquote(name) # generated by bind_quoted
def unquote(name)() do
  unquote(name)
end

The second boundry is compile time module body -> runtime function body, which is unaffected by the bind_quoted usage, because it’s another level. Nothing can persist between those two parts besides module attributes, which are available at compiletime and runtime. So you need to unquote here as well independant from if you have that code in a macro or not.

2 Likes

Sorry, I didn’t understand your post.

As far as I can tell, the only thing (and it’s not nothing) that :bind_quoted does is delay evaluation until runtime, and :bind_quoted does not in fact disable the unquote() function because calling unquote() is still required.

Looking at this code:

name = unquote(name) # generated by bind_quoted
def unquote(name)() do
  unquote(name)
end

it seems like you saying that specifying the :bind_quoted option is equivalent to calling unquote(unquote(name)). But, when I try that:

defmodule My do
  defmacro go(name) do
    quote do
      def unquote(unquote(name))() do
        unquote(unquote(name))
      end
    end
  end
end

defmodule Test do
  require My

  Enum.each [:hello, :goodbye], &My.go(&1)
end

IO.puts Test.hello
IO.puts Test.goodbye

I get:

~/elixir_programs$ iex macros2.ex 
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]

** (CompileError) macros2.ex:4: unquote called outside quote
    (stdlib) lists.erl:1354: :lists.mapfoldl/3

And, your code example doesn’t even work:

defmodule My do
  defmacro go(name) do
    quote do
      name = unquote(name)
      def unquote(name)() do
        unquote(name)
      end
    end
  end
end

defmodule Test do
  require My

  Enum.each [:hello, :goodbye], &My.go(&1)
end

IO.puts Test.hello
IO.puts Test.goodbye

Output:

~/elixir_programs$ iex macros2.ex 
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]

** (CompileError) macros2.ex:15: invalid syntax in def x1()
    (elixir) lib/enum.ex:737: Enum."-each/2-lists^foreach/1-0-"/2
    (elixir) lib/enum.ex:737: Enum.each/2
    macros2.ex:15: (module)

Edit:
Ah, but this works:

defmodule My do
  defmacro go(name) do
    quote do
      name = unquote(name)
      def unquote(name)() do
        unquote(name)
      end
    end
  end
end

defmodule Test do
  require My

  #Enum.each [:hello, :goodbye], &My.go(&1)
  My.go(:hello)
  My.go(:goodbye)
end

IO.puts Test.hello
IO.puts Test.goodbye

Because :bind_quote wasn’t specified, you don’t get the delayed evaluation until runtime.

But this works too:

defmodule My do
  defmacro go(name) do
    quote do
      #name = unquote(name)      #<=========******
      def unquote(name)() do
        unquote(name)
      end
    end
  end
end

defmodule Test do
  require My

  #Enum.each [:hello, :goodbye], &My.go(&1)
  My.go(:hello)
  My.go(:goodbye)
end

IO.puts Test.hello
IO.puts Test.goodbye

So, I guess I don’t know what unquoting something twice does.

So there are two types of unquote: The one to pass AST into a quote block and the ones that allow you to generate dynamic functions (unquote fragments). The latter don’t work with AST, but with realized compile-time values.

You’re trying to utilize both behaviors in your macro, which means you’re forced to use bind_quoted: …, because otherwise it’s ambiguous which kind of unquote() should happen.

So the following does work just using unquote fragments, having the AST type unquote() disabled because of the bind_quoted usage:

  defmacro go(name) do
    quote bind_quoted: [name: name] do
      def unquote(name)() do
        unquote(name)
      end
    end
  end

But this is also a byproduct of bind_quoted.

It’s also a shortcut, so you don’t have to manually assign all the variables your macro is using to the quoted code block:

> a = {:a, 1}
> ast = quote [bind_quoted: [a: a]], do: :some_code
> Macro.to_string(ast)
"""
(
 a = {:a, 1}
 :some_code
)
"""

You could do the same by doing:

quote do
  a = unquote(a)
  :some_code
end

Now imagine you’d do that not only with one variable, but like 7. That’s where bind_quoted is a real time/code saver. Assigning the unquoted AST to a variable is done so you can use the variable in the rest of the quote block instead of unquoting all over the place making any segments of code more unreadable and potentially calling code more often than needed.

3 Likes

The second boundry starts right after def insn’t it ? i.e. in the AST given to the def macro. Am I right ?

1 Like