Search code examples
metaprogrammingelixirphoenix-frameworkecto

How to define functions based on attribute to elixir?


Let's say I have a modules Silent and Definer. I want to define a couple of functions for Silent, based on its attribute. Let me explain:

defmodule Silent do
  @function_names [:a, :b, :c]

  use Definer
end

defmodule Definer do
  defmacro __using__(_) do
    quote do
      Enum.each(@function_names, fn(n) ->
        def unquote(n)() do # line 5
          IO.puts "a new method is here!"
        end
      end)
    end
  end
end

But this approach actually doesn't work because I have undefined function n/0 on line 5. How can I implement desired functionality?


Solution

  • You need to pass unquote: false to quote in Definer.__using__/1 to be able to inject an unquote fragment inside a quote.

    defmodule Definer do
      defmacro __using__(_) do
        quote unquote: false do
          Enum.each(@function_names, fn(n) ->
            def unquote(n)() do # line 5
              IO.puts "a new method is here!"
            end
          end)
        end
      end
    end
    
    defmodule Silent do
      @function_names [:a, :b, :c]
    
      use Definer
    end
    
    Silent.a
    Silent.b
    Silent.c
    

    prints

    a new method is here!
    a new method is here!
    a new method is here!
    

    A similar case is documented in detail in the Kernel.SpecialForms.quote/2 docs which also mentions how to use bind_quoted if you want to both inject some variables into a quote and create unquote fragments.