How to find out the arity of function passed to a macro?

I am building a macro that accepts a function. Depending on the arity of the function, I need the macro to generate different code. So far I have come up with this:

  defmacro mymacro(fun) do
    arity =
      case fun do
        {:&, _, [{:/, _, [_, arity]}]} ->
          arity

        {:fn, _, [{:->, _, [args, _]}]} ->
          length(args)
      end

    case arity do
      1 ->
        quote do
          fun.(:arg1)
        end

      2 ->
        quote do
          fun.(:arg1, :arg2)
        end
    end
  end

It works like this:

mymacro(fn a, b -> {a,b} end)
mymacro(fn a -> {a} end)
mymacro(&Atom.to_string/1)

But it has 3 problems:

  1. it does not work with anonymous functions defined using the & shorthand. I do not see a reliable way to find the arity of such functions
  2. It would not work if the argument passed to the macro is another macro because that would be just arbitrary AST
  3. It would not work if the argument passed to the macro has to be evaluated at runtime because that would be just arbitrary AST

I can live with the limitation #3.

For the other 2, I could use Code.eval_quoted to obtain the function and determine the arity, but AFAIK that’s considered bad practice and should be avoided

Another option would be to determine the arity at runtime, like this

  defmacro mymacro2(fun) do
    quote do
      f = unquote(fun)
      {:arity, arity} = Function.info(f, :arity)

      case arity do
        1 ->
          f.(:arg1)

        2 ->
          f.(:arg1, :arg2)
      end
    end
  end

but in this case dialyzer gives warnings about unmatched cases

Of course these macros are just examples to demonstrate the problem, and my actual macros are different. And in my particular case, I can think of some workarounds. So it just became kind of academic question for me, is there a generic good way to find out the arity of function passed as macro parameter at compile time?

Any hints?

Hey @ademenev is there a reason you actually need a macro? You can

case fun do
  fun when is_function(fun, 2) ->
    fun.(foo)
  fun when is_function(fun, 1) ->
    fun.(foo, bar)
end

I don’t see anything here that really requires a macro yet.

1 Like

Because I stated that is an academic problem :smile: And when solving a purely academic problem, there is no “why”, there can be either “this is how you do it” or “that’s not possible” :rofl:

Is this function always defined in the argument of a macro? Or can I do something like

f = fn x -> x + 1 end
mymacro(f)

?

Oh, I see, only static analysis.

Then you can use Tria, it has IR which has no & captures, only fn’s, which should make your life easier