@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 asMiniParams.Search
, the module being defined isElixir.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.