Please help me with macro for generating functions

Hi, I looked about generating functions and found one example. I tried to make it work inside macro, but i don’t know how I can fix it. Here is sample code that I need help with:

defmodule FirstExample do
  Enum.each [foo: [{:_arg1, [], nil}], bar: []], fn {func_name, args} ->
    def unquote(func_name)(unquote_splicing(args)), do: :ok
  end
end

defmodule SecondExample do
  defmacro my_macro(name) do
    quote do
      defmodule unquote(name) do
        Enum.each [foo: [{:_arg1, [], nil}], bar: []], fn {func_name, args} ->
          def unquote(func_name)(unquote_splicing(args)), do: :ok
        end
      end
    end
  end
end

Please explain me:

  1. How unquote works in FirstExample?
    unquote should work only inside of quote, right?
  2. How I could correct SecondExample to make it work?
    Important: I need these data to be inside defmodule unquote(name) do ... end and I need to generate whole module)?
    I can see that unquote tries to get data outside of quote, but i don’t know how I can fix it to make it work like in FirstExample.

Use Module.create/3 that, as by documentation, “Creates a module with the given name and defined by the given quoted expressions.”

defmodule SecondExample do
  defmacro my_macro(name) do
    contents =
      quote do
        Enum.map [foo: [{:_arg1, [], nil}], bar: []], fn {func_name, args} ->
          quote do: def unquote(func_name)(unquote_splicing(args)), do: :ok
        end
      end
    quote bind_quoted: [name: name, contents: contents] do
      Module.create(name, contents, Macro.Env.location(__ENV__))
    end
  end
end

defmodule Test do
  require SecondExample
  def test, do: IO.inspect SecondExample.my_macro(Foo), label: "Module content"
end

Test.test
2 Likes

@mudasobwa: Thank you. It’s really helpful, but I still can’t get it work with my code

I have 3 problems:

  1. If I place 3 times similar each call (with another function names) then only last is added as module body (I can’t run other functions in iex). I want to pass whole module body there - not only last result of quote.
  2. After change order nothing happens until I remove _build folder and compile whole project again - this is really uncomfortable.
  3. In my case there is another problem: I have stored list of that data (function names and arguments) in module attribute and i would like to keep them there.

The workaround for 1st problem:

# ...
contents = quote do
  [quote do
    # first part, define and work on module attributes and functions
  end]
  ++
  Enum.map(...) # generated functions here ...
end
# ...

but I think it’s not a best idea.

I will try to describe better how my code works. In my version these looks like:

defmodule MyLib do
  defmacro my_macro(name, do: block) do
    quote do
      defmodule unquote(name) do
        # alias, import and require here ...

        # pre work: attributes + define some methods

        _ = unquote(block)

        # post work: also attributes + define some methods

        # call macro that returns this list - IO.inspect here shows correct data,
        # but here starts my problem, because I can't use unquote here
        # and I can't use unquote_splicing if I try to place `each` call in separate macro
        # it's something like:
        result_of_my_macro = Example.parse_data(@my_data)
        Enum.each result_of_macro, fn {func_name, args} ->
          def unquote(func_name)(unquote_splicing(args)), do: :ok
        end
        # unfortunately I can't use your version (it looks really good),
        # because I have data for Enum.each inside module body
        # i.e. in same scope as definition of that functions
      end
    end
  end
end

# There are much more, but I hope that you now better understand my problem

# in another file:
require MyLib
MyLib.my_macro ModuleName do
  # here I use some macro dynamically imported into this module body
end

What do you think about it?

I’m no macro expert, but don’t you want to be using Enum.map instead of Enum.each so you can capture the generated AST?

1 Like

@gregvaughn: Originally I’m using Enum.each. I used Enum.map only to test @mudasobwa answer.

Simply in defmodule -> Enum.each works without error.

There is a problem with args, because I can’t call unquote_splicing inside defmodule -> defmacro -> quote -> Enum.each.

Please try SecondExamplecode in my first post for example in iex.

Don’t prepend the element to the list with ++, use [head | tail] notation:

contents = quote do
  [
    quote do
      # first part, define and work on module attributes and functions
    end | 
    Enum.map(...) # generated functions here ...
  ]
end

You seem to misheard what I was saying: one should not use defmodule inside macros/functions due to the unquoting scopes problems you’ve met. Use Module.create/3 in such a case. There is no way (at least legit) to unquote from the middle level of nested quoting.

Whether you are still positive you want to violate guidelines and how-tos provided by core team members, you might use a hack (read: a kludge) with Code.eval_quoted/3 as shown below:

defmodule KludgeExample do
  defmacro my_macro(name) do
    quote do
      defmodule unquote(name) do
        Enum.each [foo: [{:_arg1, [], nil}], bar: []], fn {func_name, args} ->
          ast = quote do: def unquote(func_name)(unquote_splicing(args)), do: :ok
          Code.eval_quoted(ast, [func_name: func_name, args: args], __ENV__)
        end
      end
     end
  end
end

defmodule Test do
  require KludgeExample
  def test do
    KludgeExample.my_macro(Foo)
    IO.inspect {Foo.foo(42), Foo.bar}, label: "{Foo.foo(42), Foo.bar()}"
  end
end

Test.test

In my case there is another problem: I have stored list of that data (function names and arguments) in module attribute and i would like to keep them there.

I do not see any problem here: add the module attribute declaration to the contents as shown below:

contents =
  quote do
    [ quote do
        Module.put_attribute(__MODULE__, :my_data, ...)
        # first part, define and work on module attributes and functions
      end | 
      Enum.map(...) # generated functions here ...
    ]
  end
1 Like

Appending here is totally fine. If not elixir does it, erlang will optimise it away. It will optimise appending to a singleton list into consing.

Also, there is often no problem with appending to a list once, it does get dangerous when done recursively though.

2 Likes

Yes, indeed, I should’ve put the better wording there. I meant “get a habit to use [head | tail] notation, rather than [head] ++ tail, it might save you time for not thinking whether it’s a performance hit or not.”

1 Like

I’m guessing @my_data is set by the block. You can split large quote into two quotes, and use [unquote: false] to disable unquote/1. Consider moving the first quote to a private function in MyLib for better readability and maintainability.

defmodule MyLib do
  defmacro my_macro(name, do: block) do
    function_generator =
       quote unquote: false do
         for {name, args} <- Example.parse(@my_data) do
           def unquote(name)(unquote_splicing(args)), do: :ok
         end
       end

    quote do
      defmodule unquote(name) do
        unquote(block)
        unquote(function_generator)
      end
    end
  end
end

@gregvaughn Enum.each is fine in this case as def just inserts into an ets table.

2 Likes

@mudasobwa: Thanks, but these does not look good (or another words: these could look better), but your example is very, very good for simpler cases (your first example looks really nice).

I know [head | tail] notation, but this was only example - i.e. at end I will rewrite it anyway, but I don’t know about what @NobbZ say - really interesting, thanks!

You should agree with me that @net solution looks best for my case - i.e. there is no need to care about what quoted block returns which was exactly what I want. Just now I tested it with my code - it’s really little change in this case and it works as expected. For me it looks best, so I mark it as a answer.

Thank you all for your time and help!