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?

2 Likes

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)

5 Likes

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)
1 Like

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?

1 Like

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:)

1 Like