Error in macro that defines a module with a macro inside

I have some relatively legitimate reasons do do something like this (even if you think this is not even remotely legitimate, the question at the end is independent from this):

defmodule MySnippet do
  defmacro __using__(_) do
    quote do
      def common_function1() do
        # ...
      end

      def common_function2() do
        # ...
      end
      # etc...
    end
  end
end

defmodule MyModule1 do
  use MySnippet

  def specific_function1() do
    # ...
  end

  def specific_function2() do
    # ...
  end
  # etc...
end

defmodule MyModule2 do
  use MySnippet

  def specific_function_a() do
    # ...
  end

  def specific_function_b() do
    # ...
  end
  # etc...
end

I’ve thought about defining a defsnippet macro that exapnds so that I coould do this:

require Snippet
Snippet.defsnippet MySnippet do
  def f(x), do: x
  def g(x), do: x
end

This way, it looks like a normal module definition and is easier to type.
I’ve written the following macro, which tries to encapsulate the module definition and the __using__/1 macro definition.

defmodule Snippet do
  defmacro defsnippet(module_name, [do: body]) do
    quote bind_quoted: [module_name: module_name, body: body] do
      defmodule module_name do
        @doc false
        defmacro __using__(_opts) do
          quote do
            body
          end
        end
      end
    end
  end
end

But when I try to run it like this

# lib/schism/example.ex
require Snippet
Snippet.defsnippet MySnippet do
  def f(x), do: x
  def g(x), do: x
end

I get this error:

== Compilation error in file lib/schism/example.ex ==
  ** (ArgumentError) cannot invoke def/2 outside module
      (elixir) lib/kernel.ex:5142: Kernel.assert_module_scope/3
      (elixir) lib/kernel.ex:3905: Kernel.define/4
      (elixir) expanding macro: Kernel.def/2
      lib/schism/example.ex:4: (file)
      expanding macro: Snippet.defsnippet/2
      lib/schism/example.ex:3: (file)

I don’t understand where this comes from, because I’m not calling def/2 outside of a module. I’m just taking some AST that happens to contain defs and putting it inside a macro which I can invoke later.

Doing all of this just to save some typing is probably not worth it and I don’t know if I’ll end up going that way, but no I’m curious about what the problem is independently of the rest.

I am not sure if it fixes the whole issue (I believe it does,) but you definitely should use Module.create/3 not Kernel.defmodule/2 in this context.

Eh I think defmodule is fine here.

However the issue is that you cannot use bind_quoted in this way (I learned that well long ago). Basically what bind_quoted will do is turn the code into something like this from the return of your macro:

module_name = MySnippet

body =
  (
    def f(x), do: x
    def g(x), do: x
  )

defmodule(module_name) do
  @doc false 
  defmacro(__using__(_opts)) do
    quote do
      body
    end
  end
end

And this is why you are getting the def is not allowed here message. I’ve always had to do ‘fun’ unquote messing when I had to do similar things. So if you Macro.escape the body in bind_quoted then use unquote(body) inside the function then it might work (you might need to ‘escape’ the inner quote though or something like that, I usually just don’t use bind_quoted at all in this case and just unquote through the layers as needed or build the AST manually without quote…).

1 Like

It doesn’t. In fact, defmodule doesn’t have anything to do with it. OverminDL1 has given the correct answer. Thanks for your input anyway!

1 Like