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:
- Overriding
def
,defp
, ordefmodule
is common amongst libraries that want to enhance function-definitions in some way. - Overriding builtin operators like
+/2
,-/2
or|>/2
to 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
defoverridable
allowing a library-implementer to simply callsuper(...)
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
Here, both OverrideExample1
and 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 Kernel
implementation.
Of course, this is just a single idea.
I’d love to talk about this and hear your opinions!