Defmacro inside defmacro

Hi,

how does one unquote and use a variable from outer scope in nested defmacro?

I want to have a macro, which when used like def_loggers(:core) which expands to another defmacro like defmacro core_log(format_str)

Reason for this is because I want to inject MODULE inside the parameter list… (I know there’s Logger lib, which can do that, but this is for work where I can’t change it)…

So, my attempts so far were:

1.)
defmacro def_loggers(log_prefix) do
macro_name = String.to_atom("#{log_prefix}_log")

  quote bind_quoted: [prefix: log_prefix, name: macro_name] do
    defmacro unquote(name)(format) do
      quote do
        vlinfo(unquote(unquote(prefix)), unquote("~s: " <> format), [__MODULE__])
      end
    end
  end
end

Problem is, that when I want to use def_loggers macro like this:

defmodule Xxx, do: Rezolve.XLager.def_loggers(:core)

so that I can use log macro core_log like this:

Xxx.core_log("TEST")

I’m hitting this error:

iex(core_1@elixir-node_1)55> defmodule Xxx, do: Rezolve.XLager.def_loggers(:core)
warning: redefining module Xxx (current version defined in memory)
iex:55

** (CompileError) iex:55: unquote called outside quote
(stdlib) lists.erl:1354: :lists.mapfoldl/3
(stdlib) lists.erl:1355: :lists.mapfoldl/3
iex:55: (module)
(stdlib) erl_eval.erl:670: :erl_eval.do_apply/6
(iex) lib/iex/evaluator.ex:231: IEx.Evaluator.handle_eval/5

Not quite sure why - the unquote(unquote(prefix)) - 2 levels of unquoting is inside 2 levels of quoting introduced by 2 surrounding defmacro’s.

With only one level of unquoting of prefix variable like this:

defmacro def_loggers(log_prefix) do
  macro_name = String.to_atom("#{log_prefix}_log")

  quote bind_quoted: [prefix: log_prefix, name: macro_name] do
    defmacro unquote(name)(format) do
      quote do
        vlinfo(unquote(prefix), unquote("~s: " <> format), [__MODULE__])
      end
    end
  end
end

The compile error makes more sense:

iex(core_1@elixir-node_1)60> defmodule Xxx, do: Rezolve.XLager.def_loggers(:core)
warning: redefining module Xxx (current version defined in memory)
iex:60

** (CompileError) iex:60: undefined function prefix/0
(stdlib) lists.erl:1338: :lists.foreach/2

But of course this isn’t the right way to go as well.

Any ideas?

Thanks,
Karol

1 Like

You have 2 unquote’s here, the unquote(unquote(prefix)) part, shouldn’t that just be unquote(prefix) instead?

You should only need one because the outermost scope is using bind_quoted (which escapes unquote’s at it’s level).

This reason is because the prefix is bind_quoted on the outer level and yet you are trying to unquote it into the inner context, when you probably are not wanting to unquote there, for example your code of:

defmacro def_loggers(log_prefix) do
  macro_name = String.to_atom("#{log_prefix}_log")

  quote bind_quoted: [prefix: log_prefix, name: macro_name] do
    defmacro unquote(name)(format) do
      quote do
        vlinfo(unquote(prefix), unquote("~s: " <> format), [__MODULE__])
      end
    end
  end
end

When run like Blah.def_loggers(:Vwoop) will generate:

prefix = :vwoop
name = :vwoop_log

defmacro unquote(name)(format) do
  quote do
    vlinfo(unquote(prefix), unquote("~s: " <> format), [__MODULE__])
  end
end

Which I’m guessing is not what you intended? You probably intended to use this code:

defmacro def_loggers(log_prefix) do
  macro_name = String.to_atom("#{log_prefix}_log")

  quote do
    defmacro unquote(macro_name)(format) do
      log_prefix = unquote(log_prefix)
      quote do
        vlinfo(unquote(log_prefix), "~s: " <> unquote(format), [__MODULE__])
      end
    end
  end
end

Which generates:

defmacro vwoop_log(format) do
  log_prefix = :vwoop

  quote do
    vlinfo(unquote(log_prefix), "~s: " <> unquote(format), [__MODULE__])
  end
end

For note, quotes in quotes act odd, usually better to put them in different functions instead of escape the inner quotes. :slight_smile:

1 Like

Awesome, thank you.

Elixir complained about the empty bind_quoted so I removed that and now it works fine as

defmacro def_loggers(log_prefix) do
  macro_name = String.to_atom("#{log_prefix}_log")

  quote do
    defmacro unquote(macro_name)(format) do
      log_prefix = unquote(log_prefix)
      quote do
        vlinfo(unquote(log_prefix), "~s: " <> unquote(format), [__MODULE__])
      end
    end
  end
end

Is there a good middle-level doc for metaprogramming in Elixir online? I’m aware of the book, but online source would be handy…

Cheers,
Karol

1 Like

How did that slip in?! Removed in my post. ^.^;

The official docs are good, the elixir school website is good, these forums are good, and tons of blogs and such. :slight_smile:

1 Like