elixir-lang / elixir

Elixir is a dynamic, functional language for building scalable and maintainable applications
https://elixir-lang.org/
Apache License 2.0
24.34k stars 3.36k forks source link

Impossible to use `apply/3` with macros: UndefinedFunctionError #7740

Closed eksperimental closed 6 years ago

eksperimental commented 6 years ago

Environment

Been banging my head for a while trying to figure this one out. It's either a bug or a limitation that is not properly documented.

Currently it is impossible to call a macro from apply/3

iex(1)> require Integer
Integer
iex(2)> apply(Integer, :is_odd, [1])
** (UndefinedFunctionError) function Integer.is_odd/1 is undefined or private. However there is a macro with the same name and arity. Be sure to require Integer if you intend to invoke this macro
    (elixir) Integer.is_odd(1)

the error is misleading since it has been required. The only (partial) workaround I was able to come up with was to run:

iex(1)> require Integer
iex(2)> apply(fn(x) -> Integer.is_odd(x) end, [1])
true

Expected behaviour: It should be either documented or fixed

eksperimental commented 6 years ago

I got stuck trying to create a macro that will solve this limitation..

defmodule MacroExperiment do
  defmacro apply_macro(module, macro_name, args) do
    quote do
      unquote(module).unquote(macro_name)(unquote_splicing(args))
    end
  end
end

this works when passing the values directly, but when calling storing them in a variable, it won't.

defmodule MacroExperimentTest do
  use ExUnit.Case
  require Integer
  import MacroExperiment

  test "apply_macro" do
    # it works
    assert apply_macro(Integer, :is_even, [2]) == true
    refute apply_macro(Integer, :is_even, [3])

    module = Integer
    macro_name = :is_even
    args = [2]

    # it does not work
    assert apply_macro(module, macro_name, [2]) == true
    # raises: ** (UndefinedFunctionError) function Integer.macro_name/1 is undefined or private    

    assert apply_macro(module, macro_name, args) == true
    # ** (ArgumentError) expected a list with quoted expressions in unquote_splicing/1, got: {:args, [line: 19], nil}
    #     (elixir) src/elixir_quote.erl:114: :elixir_quote.argument_error/1
    #     (elixir) src/elixir_quote.erl:95: :elixir_quote.list/2
    #     (macro_experiment) expanding macro: MacroExperiment.apply_macro/3
    #     test/macro_experiment_test.exs:19: MacroExperimentTest."test apply_macro"/1
    #     (ex_unit) expanding macro: ExUnit.Assertions.assert/1
    #     test/macro_experiment_test.exs:19: MacroExperimentTest."test apply_macro"/1
    #     (elixir) lib/code.ex:677: Code.require_file/2
    #     (elixir) lib/kernel/parallel_compiler.ex:201: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/6
  end
end

any hints how to solve this?

josevalim commented 6 years ago

You can’t. A macro needs to be invoked at compile time. You can’t invoke it at runtime, so you can’t put the values you are going to invoke in variables. --

José Valimwww.plataformatec.com.br http://www.plataformatec.com.br/Founder and Director of R&D

OvermindDL1 commented 6 years ago

Well, you 'could' write something that could invoke it at run-time (macro's are just specially named functions after all), but you'd get AST back, which is not as useful at runtime unless you want to run it through the interpreter or something (and you'd probably need to fake a chunk of the __CALLER__ environment too), which is doable though slower (and you'd need to build up the interpreter environment).

Honestly though, instead of asking how to apply a macro directly via run-time variables, you should post what you are actually trying to accomplish (not 'how') on the forums. :-)

ryanwinchester commented 6 years ago

https://en.wikipedia.org/wiki/XY_problem

The XY problem: You want to do X, but don't know how. You think you can solve it using Y, but don't know how to do that either. You ask about Y, which is a strange thing to want to do. Just ask about X.