Prevent function implementation

Is there any way to ensure that a specific function will not be implemented on module?

The example: I do not want that module A implements the function explode_system/1, and if I do this

defmodule A do
   def explode_system(system), do: IO.puts "boom!"
end

Then generates a warning or a compile time error.

Another example with macros if I have a template using use MyTemplate that inject functions I do want the target module override this functions. So, if I have this below I get a error/warning:

defmodule MyTemplate do
    defmacro __using__(_opts) do
       def explode_system(system) do
          IO.puts("Carefully explodes the system...")
       end
    end
end

defmodule MyModule do
    use MyTemplate

    # if I try to do this get an error/warning
    def explode_system(system), do: IO.puts("boom!")
end

See before_compile and Module.definitions_in

1 Like

But inside the __before_compile__/1 when I do, List.first(env.context_modules) |> Module.definitions_in() the definition injected by macro via use MyTemplate already is there.

Your example almost works; I added a quote block to make the __using__ a valid macro:

defmodule MyTemplate do
  defmacro __using__(_opts) do
    quote do
      def explode_system(system) do
        IO.puts("Carefully explodes the system...")
      end
    end
  end
end

defmodule MyModule do
    use MyTemplate

    # if I try to do this get an error/warning
    def explode_system(system), do: IO.puts("boom!")
end

running that in iex produces a warning:

warning: this clause for explode_system/1 cannot match because a previous clause at line 17 always matches
  iex:20

but just putting a guard on injected function stop the warning, and the behave wanted is none function with this name must be present in module

defmodule MyTemplate do
  defmacro __using__(_opts) do
    quote do
      def explode_system(system) when is_integer(system) do
        IO.puts("Carefully explodes the system...")
      end
    end
  end
end

A possible solution that I find is using the advice of @hst337 of use Module.definitions_in + inject a @before_compile in the module that is using the template:

defmodule MyTemplate do
  defmacro __using__(_env) do
    quote do
      @before_compile MyTemplate
    end
  end

  defmacro __before_compile__(%Macro.Env{} = env) do
    definitions =
      env.module
      |> Module.definitions_in()
      |> Keyword.keys()
      |> Enum.uniq()

    if :explode_system in definitions do
      IO.warn("Do not explode the system dude")
    end
  end

  quote do
    def explode_system() do
      # ...
    end
  end
end
3 Likes

Edit: If the opts params from defmacro __using__(opts) is needed , a alternative is setup it in a module attribute and retrieve on __before_compile__/1

defmodule MyTemplate do
  defmacro __using__(opts) do
    quote do
      @before_compile MyTemplate
      @opts unquote(opts)
    end
  end

  defmacro __before_compile__(%Macro.Env{} = env) do
    # do your stuffs
    opts = Module.get_attribute(env.module, :opts)

    quote do
      # now can use unquote(opts)
      def my_function() do
        x = unquote(opts)
      end
    end
  end
end