How to generate function stubs from list of function names with arity?

I want something like:

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

How it can be achieved?

I believe it is something like this:

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:

  1. The AST for the names arg1, arg2, ... argN are constructed from the given arity by using Macro.var.
  2. 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?

It’s generates this kind of errors:

** (ArgumentError) argument error
    :erlang.atom_to_binary({:@, [line: 81], [{:functions_list, [line: 81], nil}]}, :utf8)

If you need to do that, you must expand the name inside the quote, ie, from @Qqwy code

      send(self, {Atom.to_string(unquote(name)), unquote_splicing(arguments)})

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

How can I expand AST of arity to actual value?

Finally I’ve managed to make it work by stupidly inlining stub generation into the comprehension.

My task was massive NIF stubs generation for Matrex library.

Here’s the code:

  @types [
    "float64",
    "float32",
    "byte",
    "int16",
    "int32",
    "int64"
  ]

  @nifs [
    add_scalar: 2,
    add: 2,
    argmax: 1,
  ]

  for {name, arity} <- @nifs do
    for type <- @types do
      def unquote(:"#{name}_#{type}")(
            unquote_splicing(Enum.map(1..arity, &Macro.var(:"_arg#{&1}", nil)))
          ),
          do: :erlang.nif_error(:nif_library_not_loaded)
    end
  end

Still don’t know how to accomplish this with macros in a more elegant way:)