Hello everyone to this week’s edition of ‘metaprogramming with @qqwy’.
This week I was thinking a lot about libraries that override built-in functions or macros.
This is a relative common technique that quite a few libraries use. Two common examples are:
defmoduleis common amongst libraries that want to enhance function-definitions in some way.
- Overriding builtin operators like
|>/2to enhance the kinds of data-structures that these operators allow.
However, there is a glaring problem with this technique, and that is that a library containing a definition like this:
def a + b do if is_fancy(a) or is_fancy(b) do my_custom_logic(a, b) else Kernel.+(a, b) end end
can never be used together with another library that wants to enhance the same function, macro or operator.
The problem here is that we are blindly falling back to
Kernel, meaning that we bypass the other library(ies) that might be in scope.
How can we fix this? Great question!
One idea that I have, is described in this gist. Summarized:
- Library-modules that want to override a function or macro contain a ‘default implementation’ that looks up what earlier implementation was in scope and call that one. This default implementation can be automatically injected and annotated with
defoverridableallowing a library-implementer to simply call
super(...)whenever they want to fall back to the default implementation that is in scope.
- Library-modules add a snippet to their own
__using__macro that will hide the conflicting implementations that are in scope which would conflict with their implementations and sets up that this now-hidden implementation is what should be called whenever a fallback is triggered.
In the end we then end up with something that from the user’s perspective looks like this:
defmodule Example do use OverrideExample1 use OverrideExample2 @a 1 @b 2 end
OverrideExample2 have overridden the
@ operator macro. Since they use
SafeOverride, when the
@ macro is called inside
Example at compile-time,
OverrideExample2's implementation is used, which will fall back to
OverrideExample1's implementation, which will fall back to the
Of course, this is just a single idea.
I’d love to talk about this and hear your opinions!