Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121010

Why the dynamic call results in “undefined function”?

I have a module that dynamically routes external calls to it’s own functions in the following way:

defmodule A do
  defmacro call(name) do
    quote do
      fun = & unquote(:"A.#{name}")(&1)
      fun.(:foo)
    end
  end

  def test(param), do: IO.inspect(param, label: "test")
end
#⇒ {:module, A, ..., {:test, 1}}

The module was successfully compiled and A.test/1 is there.

A.test :foo
#⇒ test: :foo

Now I try to call it as:

defmodule B do
  require A
  def test, do: A.call(:test)
end
#⇒ ** (CompileError) iex:21: undefined function A.test/1
#      (stdlib) lists.erl:1338: :lists.foreach/2
#      (stdlib) erl_eval.erl:677: :erl_eval.do_apply/6
#      (iex) lib/iex/evaluator.ex:249: IEx.Evaluator.handle_eval/5

What is wrong with this dynamic call dispatch and why the error message contradicts the reality?

Upvotes: 1

Views: 96

Answers (1)

Dogbert
Dogbert

Reputation: 222388

The error message is misleading. & unquote(:"A.#{name}")(&1) will call a function literally named A.test in the current scope, not the test/1 function of module A:

defmodule A do
  defmacro call(name) do
    quote do
      fun = & unquote(:"A.#{name}")(&1)
      fun.(:foo)
    end
  end

  def unquote(:"A.test")(param), do: IO.inspect(param, label: "!!!")
end

defmodule B do
  require A
  import A
  def test, do: A.call(:test)
end

B.test

Output:

!!!: :foo

To make it call the test/1 function of module A, you can do & A.unquote(:"#{name}")(&1):

defmodule A do
  defmacro call(name) do
    quote do
      fun = & A.unquote(:"#{name}")(&1)
      fun.(:foo)
    end
  end

  def test(param), do: IO.inspect(param, label: "test")
end

defmodule B do
  require A
  def test, do: A.call(:test)
end

B.test

Output:

test: :foo

Upvotes: 2

Related Questions