defmodule Interface do
def function_list, do: [first_func_name: 2, second_func_name: 3]
end
defmodule SelfStub do
for {func_name, arity} <- Interface.function_list do
generate_func_macro(func_name, arity)
end
end
To result in:
defmodule SelfStub do
def first_func_name(arg1, arg2) do
send(self, {"first_func_name", arg1, arg2})
end
def second_func_name(arg1, arg2, arg3) do
send(self, {"second_func_name", arg1, arg2, arg3})
end
end
def arguments_for_arity(arity) do
1..arity
|> Enum.map(fn argnum ->
:"arg#{argnum}"
|> Macro.var(__MODULE__)
end)
end
def generate_func_macro(name, arity) do
name_str = name |> Atom.to_string
arguments = arguments_for_arity(arity)
quote do
def unquote(name)(unquote_splicing(arguments)) do
send(self, {name_str, unquote_splicing(arguments)})
end
end
end
So two ‘tricks’ happen here:
The AST for the names arg1, arg2, ... argN are constructed from the given arity by using Macro.var.
unquote_splicing is used to insert the argument list as separate parameters both in the function definition as well as the tuple that is going to be sent.
Note that generate_func_macro is not actually a macro; it is a function returning AST (but it does not take AST as input). As this function is called outside of a function definition, the AST it returns is just embedded as-is in the module.
(Now I hope there are no syntax errors in my code; I cannot easily run it right now)
Thank you, it should work fine with raw values. But I pass function or variable to macros, so it receives raw AST as parameters. How can I expand AST to actual value?
Thank you @vic. It not obvious for me in case of passing arity to macros as function or variable.
ExUnit.start
defmodule GenTest do
use ExUnit.Case
defmodule Helpers do
def get_args(arity) do
Enum.map 1..arity, &(Macro.var(:"arg#{&1}", __MODULE__))
end
defmacro gen_func(name, arity) do
args = get_args(arity)
quote do
def unquote(name)(unquote_splicing(args)) do
:ok
end
end
end
end
defmodule A do
import Helpers
@function_list [
first: 1,
second: 2
]
for {name, arity} <- @function_list do
gen_func(name, arity)
end
end
test "A.func" do
IO.inspect A.__info__(:functions)
assert :ok == A.first(nil)
assert :ok == A.second(nil, nil)
end
end