Manipulating code tuple inside a macro

I’m not looking for an answer for what I’m trying to do. Instead, I want to know why I’m getting an error.

I’m trying to return a def from a macro, but I want to change the name of the def in the code tuple before returning the code tuple from the macro–but I’m getting an error. Here’s my code:

defmodule My do
  defmacro times_n(n) do

    quote do
      def times(x) do
        x * unquote(n)
      end
    end
    |> IO.inspect
    |> replace_func_name(n)
    |> IO.inspect 

  end

  def replace_func_name(
    {
       :def, 
       context, 
       [
         {func_name, context_import, x}, 
         sublist
       ]
    },
    n
  ) do

    {
      :def, 
      context, 
      [
        {:"#{func_name}_#{n}", context_import, x},
        sublist
      ]
    }

  end 

end

defmodule Test do
  require My
  My.times_n(5)
end

IO.puts My.times_5(3)

Here’s the output I see:

~/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]

    {:def, [context: My, import: Kernel],
     [
       {:times, [context: My], [{:x, [], My}]},
       [do: {:*, [context: My, import: Kernel], [{:x, [], My}, 5]}]
     ]}

    {:def, [context: My, import: Kernel],
     [
       {:times_5, [context: My], [{:x, [], My}]},
       [do: {:*, [context: My, import: Kernel], [{:x, [], My}, 5]}]
     ]}

    ** (UndefinedFunctionError) function My.times_5/1 is undefined or private. Did you mean one of:

          * times_n/1

        My.times_5(3)
        macros2.ex:39: (file)
        (elixir) lib/code.ex:677: Code.require_file/2

It looks like I’ve correctly changed the name of the function in the code tuple, but I still get the undefined function error for My.times_5(). Can someone tell me why that is?

You are calling the macro My.times_n/1 while defining the Test module, so the AST returned by that macro is injected into Test.

Try Test.times_5(3) instead.

Heck! Thanks!

Okay, why doesn’t this work:

defmodule Test do
  require My
  My.times_n(5)
  IO.puts times_5(3)   # line 36
end

** (CompileError) macros2.ex:36: undefined function times_5/1

Because when you try to call that function, the module does not yet exist.

You can’t use something from the same context you are still defining.

1 Like

You are right again! Proof:

defmodule My do
  def go, do: IO.puts "hello"
  go()
end

$ iex my.exs

Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]

** (CompileError) my.exs:3: undefined function go/0

Thanks!

And now for the easy way (I finally figured out the syntax!):

defmodule My do
  defmacro times_n(n) do
    quote do
      def unquote(:"times_#{n}")(x), do: x * unquote(n)
    end
    #|> IO.inspect
  end
end

defmodule Test do
  require My
  My.times_n(5)
end

IO.puts Test.times_5(3)
~/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]

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

Does anyone know why this doesn’t work:

quote do
  def :"times_#{ unquote(n) }"(x), do: x * unquote(n)
end

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

For the same reason why def :foo(x), do: x does not work. The name of a funtion is not an atom-literal.

Well, I was trying to figure that one out, and based on this working version:

defmodule My do
  defmacro times_n(n) do
    quote do
      def unquote(:"times_#{n}")(x), do: x * unquote(n)
    end
    #|> IO.inspect
  end
end

defmodule Test do
  require My
  My.times_n(5)
end

IO.puts Test.times_5(3)

…it seemed that a function name could be an atom.

Yes, function names are atoms, but atom-literals are not allowed there.

The following being the atom-literal?

def :"times_#{ unquote(n) }"(x)

While this line:

def unquote(:"times_#{n}")(x)

injectes an atom into the function’s AST?

1 Like