3

On compilation stage I can easily produce functions with:

defmodule A1 do
  defmodule A2 do
    Enum.each %{m: 42}, fn {k, v} ->
      def unquote(k)(), do: unquote(v)
    end 
  end 
end
IO.puts A1.A2.m
#⇒ 42

Also, I can produce modules with functions from within a function call:

defmodule B1 do
  def b2! do
    defmodule B2 do
      # enum is for the sake of future example
      Enum.each %{m1: 42}, fn {_k, v} ->
        # def b2(), do: unquote(v) WON’T WORK (WHY?), BUT
        @v v
        def b2(), do: @v
      end 
    end 
  end 
end
B1.b2! # produce a nested module
IO.puts B1.B2.b2 # call a method
#⇒ 42

Now my question is: how can I dynamically produce a module with dynamically created function names, e. g.:

defmodule B1 do
  def b2! do
    defmodule B2 do
      Enum.each %{m1: 42, m2: 3.14}, fn {k, v} ->
        @k k
        @v v
        def unquote(@k)(), do: @v # THIS DOESN’T WORK
      end 
    end 
  end 
end

NB I was able to achieve what I wanted with

defmodule B1 do
  def b2! do
    defmodule B2 do
      Enum.each %{m1: 42, m2: 3.14}, fn {k, v} ->
        ast = quote do: def unquote(k)(), do: unquote(v)
        Code.eval_quoted(ast, [k: k, v: v], __ENV__)
      end
    end 
  end 
end

but it seems to be quite hacky.

Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160

1 Answers1

4

I believe this happens due to nested macro invocations (def and defmodule are both macros). If you place an unquote there, it unquotes from the top level def:

defmodule B1 do
  k = :foo
  v = :bar
  def b2! do
    defmodule B2 do
      def unquote(k)(), do: unquote(v)
    end
  end
end

B1.b2!
IO.inspect B1.B2.foo

prints

:bar

The Module.create/3 recommends using that function to dynamically create modules when the body is an AST. With that, the code becomes much more elegant than the hacky solution using Code.eval_quoted/3:

defmodule B1 do
  def b2! do
    ast = for {k, v} <- %{m1: 42, m2: 3.14} do
      quote do
        def unquote(k)(), do: unquote(v)
      end
    end 
    Module.create(B1.B2, ast, Macro.Env.location(__ENV__))
  end 
end

B1.b2!
IO.inspect B1.B2.m1
IO.inspect B1.B2.m2

Output:

42
3.14
Dogbert
  • 212,659
  • 41
  • 396
  • 397
  • Brilliant. Thank you! For an unknown reason I was looking for `Module.create`-like function in `Macro` module. – Aleksei Matiushkin Jun 30 '17 at 17:47
  • FWIW I spent 30+ minutes trying to figure this out the day you posted this question but couldn't find a solution. Yesterday I was reading some other documentation and stumbled upon `Module.create/3` and then remembered this question. :) – Dogbert Jul 01 '17 at 03:30