Pass module to a macro

Hi all
I want to pass a module to a macro and inside the macro, it should call a function from the passed module.
The following code snippet should clarify what I mean.
This module I will pass to macro

defmodule Plug do
  def init do
    5
  end
end

and the macro

defmodule Macros do
  defmacro cosume(module) do
    var = module.init
    quote do
      unquote(var)
    end
  end
end

I compiled everything and tried to use the macro:

iex(1)>  import_file("macros.exs")
{:module, Macros,
 <<70, 79, 82, 49, 0, 0, 5, 208, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 158,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:consume, 1}}
iex(2)>  import_file("plug.exs")  
{:module, Plug,
 <<70, 79, 82, 49, 0, 0, 4, 140, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 127,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:init, 0}}
iex(3)> import Plug
Plug
iex(4)> require Macros
Macros
iex(5)> Macros.consume(Plug)
** (UndefinedFunctionError) function :__aliases__.init/1 is undefined (module :__aliases__ is not available)
    :__aliases__.init({:__aliases__, [counter: 0, line: 5], [:Plug]})
    expanding macro: Macros.consume/1
    iex:5: (file)
iex(5)> 

What am I doing wrong?

Thanks

2 Likes

You don’t get values into a macro but ASTs.

The AST generated by the term Plug is {:__aliases__, [alias: false], [:Plug]}. To get the actual value out of it, you need to expand it.

A small iex session to show it a little bit better:

iex(1)> quote do: Plug    
{:__aliases__, [alias: false], [:Plug]}
iex(2)> (quote do: Plug) |> Macro.expand(__ENV__)
Plug

As I told you a couple of weeks ago, you need to make clear to yourself the differences of compile and runtime as well as the different forms of representing data at the various stages of the lifecycle of a programm from source to executable.

3 Likes

@NobbZ Can you point me to the thread you are referring too, for my own edification.

This is the thread I mean, but I havent said it as urgent as I thought I did.

@kostonstyle, @StevenXL -

For anyone looking to use macros / metaprogramming, there’s a great resource:

Metaprogramming Elixir

The e-book is just $11, and very much worth the investment (and then some!)

Also, Saša Jurić’s articles about building Macros are great.

# plug.ex
defmodule Plug do
  def init do
    5
  end
end

# macros.ex
defmodule Macros do
  defmacro cosume(module) do
    quote(bind_quoted: [module: module])  do
      # this makes most sense to me
      apply module, :init, []
      # alternatively use:
      # module.init
    end
  end
end

By using bind_quoted you are giving up control about when exactly the parameter gets expanded. In most cases you can ignore this fact, but sometimes expanding has side effects which you dont want to have.

defmodule Foo do
  defmacro unless(cond, [do: branch, else: other_branch]) do
    quote bind_quoted: [cond: cond, branch: branch, other_branch: other_branch] do
      if cond, do: other_branch, else: branch
    end
  end
end

require Foo
Foo.unless true do
  IO.puts "Branch"
else
  IO.puts "Other Branch"
end

This will probably not do what you want (perhaps there is even refinement necessary, since I have blindly typed without beeing able to do a testrun). This has already been discussed about 8 weeks ago in another thread and might give you some insights as well:

I would argue that bind_quoted makes the code more readable and should be used unless there were some special considerations to be taken. I gather it is quite easy to reason that binding a quoted expression evaluation to a variable must include running that code.

So I would not advice against using it in general :slight_smile:

I am reading the article from Saša Jurić about Meta programming and try to understand the differences of compile and runtime.
Look at the following example:

defmodule Tracer do
  defmacro trace(expression_ast) do
    string_representation = Macro.to_string(expression_ast)

    quote do
      result = unquote(expression_ast)
      Tracer.print(unquote(string_representation), result)
      result
    end
  end

  def print(string_representation, result) do
    IO.puts "Result of #{string_representation}: #{inspect result}"
  end
end

I save this file as tracer.ex. Then I compile the file into the console as follow:

iex(1)> c("tracer.ex")
[Tracer]

Got the compiled file Elixir.Tracer.beam. How it looks inside the defmacro trace now? The code does not got expanded yet right?

After the require Tracer, I would make sure that the module is available at compile time but the module is already compiled or not? Got the information about require from here http://elixir-examples.github.io/examples/alias-use-import-require.

At the end, I call the macro.

iex(3)> Tracer.trace(1 - 3)
Result of 1 - 3: -2

My question, when I call the macro, what does the compiler? What makes me confuse is, why do I have to require Tracer, when the file is already compiled.

When I import the file, the module is getting compiled

1 Like

Compiletime is when you compile your code, runtime is when you actually run it, simple as hell, isn’t it?

By doing c "tracer.ex" you are compiling all modules that are defined in the file tracer.ex (in elixir there can be multiple modules per file). The module(s) defined in tracer.ex are not loaded at this point of time.

There are now at least 2 functions in Tracer (defmacro creates a def with additional boilerplate code for handling ASTs and contexts), trace/1 does return an AST and as such can be used at compile time to inject code in foreign modules when called.

By doing require Tracer you do actually tell the compiler to load that module Tracer, if there is no such modules BEAM file available, compilation of the current file will pause until another process has spit out that modules BEAM-file. From this moment on, functions and macros of this module are available to get called at compile time.

To actually understand whats happening on your third line of interactive input, you need to know, that iex does create an “anonymous” module, compiles it and calls into it directly. Therefore, compile and runtime are somewhat interleaved and hard to distinguish in IEx. And I will try to tear that steps apart and explain them discrete.

Also a small additional note I have to do: When describing a macro as beeing a function with additional context, I will imply, that this context appears magically and do not a full expansion.

First we do compile that line, this does involve expanding Tracer.trace(1 - 3), which is by doing the following function call: Tracer.trace({:-, [context: Elixir, import: Kernel], [1, 3]}). As you defined the macro the variable string_representation will get assigned the value "1 - 3" during compile time. Inside the quote nothing gets evaluated but transformed to an AST. The AST returned in this particular example will look like this:

{:__block__, [],
 [{:=, [],
   [{:result, [], Elixir}, {:-, [context: Elixir, import: Kernel], [1, 3]}]},
  {{:., [], [{:__aliases__, [alias: false], [:Tracer]}, :print]}, [],
   ["1 - 3", {:result, [], Elixir}]}, {:result, [], Elixir}]}

Now, this AST gets injected, compiled into BEAM and compiletime is over. Now runtime.

The AST above does resemble roughly the following code injected:

result = 1 - 3
Tracer.print("1 - 3", result)
result

This simple gets executed as if you had typed it into IEx line by line, but intermediate values are omitted.

And now lets chack if you understood this and the consequences by giving you some homeworks:

iex(1)> c("tracer.ex")
[Tracer]
iex(2)> require Tracer
nil
iex(3)> foo = 1 - 3  
-2
iex(4)> Tracer.trace(foo)

What do you expect the output of line 4 to be? Can you explain why?

1 Like

Hi @NobbZ
About homework. I am expecting at line 4 an error, because the foo is not bound.
With bind_quoted should solve the problem.

This is what I exactly need to here. Thanks so much @NobbZ.