How do I write a macro that dynamically defines a public or private function?

I’m trying to write a couple macros that define either a public or private functions. Here’s an example

defmodule Defs do
  defmacro mydef(name, do: block) do
    quote do
      def unquote(name), do: unquote(block)
    end
  end

  defmacro mydefp(name, do: block) do
    quote do
      defp unquote(name), do: unquote(block)
    end
  end
end

# Example usage
mydefp hello do
 :world 
end 

Since the functions that are being written are exactly the same except for def/defp, I’d like to extract the function writing logic into a common place

defmodule Defs do
  defmacro mydef(name, do: block), do: do_def(name, block, :def)

  defmacro mydefp(name, do: block), do: do_def(name, block, :defp)

  defp do_def(name, block, visibility) do
    #What goes here?
  end
end

Since def is just a macro on Kernel, I rewrote my macros to look like they were writing function calls.

defmodule Defs do
  defmacro mydef(name, do: block) do
    quote do
      Kernel.def(unquote(name), do: unquote(block))
    end
  end

  defmacro mydefp(name, do: block) do
    quote do
      Kernel.defp(unquote(name), do: unquote(block))
    end
  end
end

From there, it seemed that I should be able to use apply/3 to dynamically call def or defp.

defmodule Defs do
  defmacro mydef(name, do: block), do: do_def(name, block, :def)

  defmacro mydefp(name, do: block), do: do_def(name, block, :defp)

  defp do_def(name, block, visibility)
    apply(Kernel, unquote(visibility), [unquote(name), do: unquote(block)])
  end
end

However, when I use this macro in a module:

defmodule Greeter do
  import Defs
  
  mydef hello do
    :world 
  end

I get the following error:

** (CompileError) iex:3: undefined function hello/0
    (stdlib) lists.erl:1354: :lists.mapfoldl/3
    (stdlib) lists.erl:1355: :lists.mapfoldl/3
    (my_app) expanding macro: Defs.mydef/2

Long question short, how do I dynamically call def or defp so I can share the rest of the logic between my macros?

In your second example, you seem to have lost the quote blocks around the macro definitions, which is probably the source of your problems.

In this example, I was thinking the unquote would go inside do_def/3.

The main issue here is: one cannot unquote outside of the quote. That said, you cannot call unquote inside your private function. The good news would be, you don’t need it at all, just construct an AST:

defmodule Defs do
  defmacro mydef(name, do: block),
    do: do_def(name, block, :def)

  defmacro mydefp(name, do: block),
    do: do_def(name, block, :defp)

  defp do_def(name, block, visibility) do
    {{:., [], [{:__aliases__, [alias: false], [:Kernel]}, visibility]},
      [], [name, [do: block]] }
  end
end

defmodule Greeter do
  import Defs
  
  mydef hello do
    :world 
  end
end

IO.puts Greeter.hello
#⇒ world
2 Likes

There is nothing preventing you from calling