Unexpected generated module name when playing with AST

@Marcus Thanks you for the explanation. I think I got your points:

  • Module names are different from plain atoms. For example:
    • Thing is a module name. Internally, it’s an atom - :"Elixir.Thing".
    • :Thing is a plain atom.
iex> Thing == :Thing
false

iex> Thing == :"Elixir.Thing"
true
  • A module name can be converted to an atom which is not prefixed by Elixir:
iex> :"#{inspect(Thing)}" == :Thing
true
  • Module.concat returns a module name.
  • When passing a module name to defmodule unquote(module_name), such as MiniParams.Search, the module being defined is Elixir.MiniParams.Search.

Above facts clears my confusions.


To emulate the behavior of nested modules, I also explored the quote expressions generated by different code.

Let’s see the code with problem:

defmodule MiniParams do
  defmacro defparams(name, do: block) when is_atom(name) do
    module_name = Module.concat([__MODULE__, Macro.camelize("#{name}")])

    ast =
      quote do
        defmodule unquote(module_name) do  # notice this line
          def echo() do
            unquote(block)
            IO.inspect(__MODULE__)
          end
        end

        # other code will operated on above generated module
        # ...
      end

    IO.inspect(ast)

    ast
  end
end

defmodule ControllerA do
  require MiniParams

  MiniParams.defparams :search do
    IO.inspect("search defined in ControllerA")
  end
end

Above code generates the following AST:

{:defmodule, [context: MiniParams, import: Kernel],
 [
   MiniParams.Search,  # notice this line.
   [
     do: {:def, [context: MiniParams, import: Kernel],
      [
        {:echo, [context: MiniParams], []},
        [
          do: {:__block__, [],
           [
             {{:., [line: 7], [{:__aliases__, [line: 7], [:IO]}, :inspect]},
              [line: 7], ["search defined in ControllerA"]},
             {{:., [], [{:__aliases__, [alias: false], [:IO]}, :inspect]}, [],
              [{:__MODULE__, [if_undefined: :apply], MiniParams}]}
           ]}
        ]
      ]}
   ]
 ]}

Next, let’s see the code without problem:

defmodule MiniParams do
  defmacro defparams(name, do: block) when is_atom(name) do
    module_name = Module.concat([__MODULE__, Macro.camelize("#{name}")])

    ast =
      quote do
        defmodule MiniParams.Search do   # notice this line, I hardcode the module name.
          def echo() do
            unquote(block)
            IO.inspect(__MODULE__)
          end
        end

        # other code will operated on above generated module
        # ...
      end

    IO.inspect(ast)

    ast
  end
end

defmodule ControllerA do
  require MiniParams

  MiniParams.defparams :search do
    IO.inspect("search defined in ControllerA")
  end
end

Above code generates the following AST:

{:defmodule, [context: MiniParams, import: Kernel],
 [
   {:__aliases__, [alias: false], [:MiniParams, :Search]}, # notice this line
   [
     do: {:def, [context: MiniParams, import: Kernel],
      [
        {:echo, [context: MiniParams], []},
        [
          do: {:__block__, [],
           [
             {{:., [line: 7], [{:__aliases__, [line: 7], [:IO]}, :inspect]},
              [line: 7], ["search defined in ControllerA"]},
             {{:., [], [{:__aliases__, [alias: false], [:IO]}, :inspect]}, [],
              [{:__MODULE__, [if_undefined: :apply], MiniParams}]}
           ]}
        ]
      ]}
   ]
 ]}

Diff the generated AST generated by two pieces of code, we can find the difference (which are marked by the comment #notice this line):

AST #1: MiniParams.Search
AST #2: {:__aliases__, [alias: false], [:MiniParams, :Search]}

If I want to emulate the behavior of nested modules like:

defmodule A do
  defmodule B do
  end
end

I have to generate AST like AST #2.


In my case, a possible solution is:

defmodule MiniParams do
  defmacro defparams(name, do: block) when is_atom(name) do
    aliases = {
      :__aliases__,
      [alias: false],
      [:"#{inspect(__MODULE__)}", :"#{Macro.camelize(to_string(name))}"]
    }

    quote do
      defmodule unquote(aliases) do
        def echo() do
          unquote(block)
          IO.inspect(__MODULE__)
        end
      end
    end
  end
end

defmodule ControllerA do
  require MiniParams

  MiniParams.defparams :search do
    IO.inspect("search defined in ControllerA")
  end
end

defmodule ControllerB do
  require MiniParams

  MiniParams.defparams :search do
    IO.inspect("search defined in ControllerB")
  end
end

Although above code and @Marcus code work, but I still prefer my original way to generate module_name:

module_name = Module.concat([__CALLER__.module, __MODULE__, Macro.camelize("#{name}")])

Because it uses the public API of Elixir, without depending the internal machenism.

2 Likes