How to dynamically define functions

For variable number of arguments insertion you mind find looking at Macro.unquote_splicing helpful.

1 Like

@kip thanks for the hint :+1:

Tried following

defmodule MyMacros do

  defmacro gen_fun(name, args) do
    quote do
      def unquote(name)(unquote_splicing(args)) do
        :ok
      end
    end
  end

end

defmodule MyModule do

  require MyMacros
 
  fname = :abc
  fargs = [:a, :b]
  MyMacros.gen_fun(fname, fargs)

end

But it is not compiling!

== Compilation error in file lib/mymodule.ex ==
** (ArgumentError) expected a list with quoted expressions in unquote_splicing/1, got: {:fargs, [line: 30], nil}
    (elixir) src/elixir_quote.erl:122: :elixir_quote.argument_error/1
    (elixir) src/elixir_quote.erl:103: :elixir_quote.list/2
    expanding macro: MyMacros.gen_fun/2
    lib/mymodule.ex:30: MyModule(module)
** (exit) shutdown: 1
    (mix) lib/mix/tasks/compile.all.ex:59: Mix.Tasks.Compile.All.do_compile/4
    (mix) lib/mix/tasks/compile.all.ex:24: anonymous fn/1 in Mix.Tasks.Compile.All.run/1
    (mix) lib/mix/tasks/compile.all.ex:40: Mix.Tasks.Compile.All.with_logger_app/1
    (mix) lib/mix/task.ex:331: Mix.Task.run_task/3
    (mix) lib/mix/tasks/compile.ex:96: Mix.Tasks.Compile.run/1
    (mix) lib/mix/task.ex:331: Mix.Task.run_task/3
    (iex) lib/iex/helpers.ex:104: IEx.Helpers.recompile/1

It seems I need to do something similar to unquote_splicing(unquote(args))

What am I missing?

The key is is to use bind_quoted like:

defmodule MyMacros do
  defmacro gen_fun(name, args) do
    quote bind_quoted: [name: name, args: args] do
      def unquote(name)(unquote_splicing(args)) do
        :ok
      end
    end
  end
end

Which I think works as you expected. Why is that?

def is itself a macro. So now we need a way to may unquote unambiguous. Using bind_quoted: [bindings] effective disallows unquoting in the current context. And therefore unquoteing happens from the surrounding context. Its a bit mind-warping I know.

In general its a good idea to use bind_quoted because it makes clear the parameters to your macro and helps keep everything hygienic. And because José wrote the documentation that says its a good idea :slight_smile:

3 Likes

As a follow on: I mentioned that def is also a macro. Which means that you don’t actually need to define a macro as you did before. It definitely better encapsulates your intent and simplifies your code. But as a counter example consider the following:

defmodule MyMacros do
  def gen_fun(name, args) do
    quote do
      def unquote(name)(unquote_splicing(args)) do
        :ok
      end
    end
  end
end

defmodule MyModule do=
  fname = :abc
  fargs = [:a, :b]
  Code.eval_quoted MyMacros.gen_fun(fname, fargs), [], __ENV__
end

You can see:

  1. The use of unquote inside a function, not a macro
  2. The quote returns Elixir AST as always
  3. So gen_fun/2 returns an AST that expands to a function definition
  4. Now in the consuming module we have a way to get the AST we want, representing the function definition. But since its not a macro we need a way to insert that AST into the module. If it was defined in a macro then elixir would know to just insert the AST. But in this case its a function return that just happens to be AST.
  5. So we use Code.eval_quoted that actually executes the AST with the side effect that a function is defined.

I am not at all advocating this approach. Just using is as a further example of AST generation and insertion.

4 Likes

Wow, thanks a lot @kip :+1:

Code.eval_quoted took it to a whole new level. I need to learn a lot still it seems :slight_smile:. I understand these aren’t the way to code readable, maintainable and suntainable way. But still


  • What other down sides may be with this approach?
  • Are there any recommended way of AST generation and insertion?
  • Are there any use case where such AST generation and insertion can be recommended design approach (e.g. wrapping all exported functions of an erlang module)

I highly recommend Metaprogramming Elixir by @chrismccord if these are topics you’re into (which seems a safe bet at this point).

4 Likes