I'm trying to build a macro that would transform Elixir code into some kind of "configuration" for later use, so this code:
build_config with one_and_two <- SomeModule.run("one", "two"),
_three_and_four <- SomeModule.run(one_and_two, "four"),
_six_and_five <- SomeModule.run("six", "five") do
IO.puts(one_and_two)
end
is going to be represented as
[
{:one_and_two, SomeModule, :run, ["one", "two"]},
{:_three_and_four, SomeModule, :run, [{:from, :one_and_two}, "four"]},
{:_six_and_five, SomeModule, :run, ["six", "five"]}
]
The issue with such approach is that when I run mix dialyzer
, it won't complain about obviously bad types, but when I run regular code, it does complain.
For instance, I have the following code in SomeModule:
defmodule SomeModule do
@spec run(binary, atom) :: binary
def run(arg1, arg2) do
IO.puts("RUN FOR #{arg1} AND #{arg2}")
arg1 <> Atom.to_string(arg2)
end
end
and the regular code that makes Dialyzer complain is this:
one_and_two = SomeModule.run("one", "two")
SomeModule.run(one_and_two, "four")
I tried to oversimplify the macro I built, but it still a bit too complex:
defmacro build_config({:with, _meta, args}, do: _block) do
{lines, _variables_map} =
Enum.map_reduce(args, MapSet.new(), fn arg, names ->
{:<-, _,
[
{name, _, nil},
{{:., _, [{:__aliases__, _, [_module]}, _function]} = call, meta, variables}
]} = arg
names = MapSet.put(names, name)
new_variables =
Enum.map(variables, fn
{variable_name, _, nil} = variable ->
if variable_name in names do
{:from, variable_name}
else
variable
end
other ->
other
end)
{module, function, params} = Macro.decompose_call({call, meta, new_variables})
line =
quote do
{unquote(name), unquote(module), unquote(function), unquote(params)}
end
{line, names}
end)
lines
end
A repository on Github with the sample code: https://github.com/denispeplin/config_generator
My question isn't about the macro itself, but about the whole approach: if a macro outputs something that isn't executable "code", but some kind of "configuration", that doesn't call anything by itself, should Dialyzer complain about type errors?
Is there any way to achive what I want: generate a configuration from code and have Dialyzer working at the same time?
The MRE of the macro above can be reduced to a one-liner returning a tuple. The question, in general, has nothing to do with a macro, nor with its return type. Macro does inject an AST in place, there is no magic, dialyzer has no idea about macros and it works with the injected code.
Your macro outcome is just a tuple. You didn’t show how you are to use this tuple, but I would guess it’d be via
Kernel.apply/3
, which has alist()
type as a third parameter.There is no such type in erlang (and hence in elixir) as a list of fixed length with specified types. That’s why there is no way to check elements in the list of arguments in the approach you introduced above.
What you might do though, would be to introduce your own
apply/3
macro, which would expand arguments inplace withunquote_splicing/1
. Then the injected code would be similar to the regular code with arguments passed one-by-one, not as a list, and therefore dialyzer would be able to check their types.My advice would be to avoid this path until you have an absolutely clear understanding of how macros work and use somewhat less cumbersome.