How to use macro for defining multiple types for protocol to implement

Hello! I am trying to define a list of types that i want a protocol to implement like in the following simplified example

defmacro types, do: [String, Integer, Binary]

When i use it:

defimpl Test, for: TestTypes.types() do

i get the error:

    The following arguments were given to Module.concat/2:

        # 1

        # 2
        [String, Integer, Binary]

    Attempted function clauses (showing 1 out of 1):

        def concat(left, right) when (is_binary(left) or is_atom(left)) and (is_binary(right) or is_atom(right))

    (elixir 1.15.7) lib/module.ex:886: Module.concat/2
    lib/<removed>/<removed>.ex:17: (file)
    (elixir 1.15.7) lib/kernel/parallel_compiler.ex:377: anonymous fn/5 in Kernel.ParallelCompiler.spawn_workers/8

However, if i just use the list directly it works:

defimpl Test, for: [String, Integer, Binary] do

Any ideas? :sunny:

Analyze of problem

Let’s see how your code is seen by Elixir

Firstly the macro defimpl passes data together with a __CALLER__ to a function in Protocol module.

Secondly the function in Protocol module is trying to validate the input, but it does not found any problem - which is wrong in your case, but we will address it soon:

Due to fail in the expansion and validation of your data the following function clause do not match:

Finally the code reaches last function clause. Within a quote call it unquotes your data which is therefore seen as a full list instead of one of it’s elements (see failed function clause above).

Since the list is not supported as 2nd argument in Module.concat/2 definition the guard fails.

Ok, but what’s causing the problem then?

Simply all data passed into macros are either literals or AST (Abstract Syntax Tree) representing in your case a macro call. As already said defimpl-related code fails to properly validate the input data and proceeds with it further.

So what can you do about it?

There are 2 options:

  1. The first is to put a defimpl call inside your quote call. Since the argument will then be unquote(TestType.types()). The unquote call is evaluated when the quote call is returned i.e. before defimpl is ever called (something like in a templates, but for Elixir code) and then the argument passed to macro is exactly as same as you would enter it manually.

  2. A second way is similar. All you have to do is to put a defimpl into a for loop and unquote each of list elements separately. Since we also need to use unquote the solution is really similar, but in this case we unquote every element in list rather than a whole list.

The other difference between those 2 ways is that the first one causes a change at macro level (inside a hex package or other type of shared code) and the second requires to modify a final code which could be maintained by you or users of your hex package.

Helpful resources

  1. Syntax reference guide
  2. Macro.quoted_literal?/1 function documentation
  3. Quote and unquote guide guide

Thanks for the detailed explanation, very helpful.