A valuable debate arose from my last post about extending inherited
. Matz suggested using an anonymous class generated by a method from which the actual class inherits. This is a common pattern found in many gems, even my own.
class Memo::Render < Render[ engine: :json ] end
As a reminder, the behavior I want is as follows.
Render.engine #=> nil Memo::Render.superclass #=> Render Memo::Render.engine #=> :json
Note that the whole point of this debate is about Memo::Render
, which must inherit from Render
in some way or another and has to be configured differently from its superclass: its engine
returns :json
.
“Inherit-then-tune”
Here’s a quick example how this is done currently. In a private discussion with Piotr Solnica, who’s been a great help for many years by letting me bounce off my stupid ideas, we ended up using the term “inherit-then-tune” for this pattern.
class Render singleton_class.attr_accessor :engine def self.[](engine:) klass = Class.new(Render) # inherit klass.engine = engine # tune klass end def self.inherited(subclass) super subclass.engine = self.engine # inherit end end
We need a class accessor for the engine
variable (line 2). A magic Render::[]
method creates an anonymous class, sets the configuration variables (“tunes”), and returns it (line 4-8). When deriving Memo::Render
, it inherits from this anonymous class.
As a third step, we need to implement inheriting these config variables (line 10-13). This might happen automatically depending on your framework code, but it is a step you have to consider as well.
Nothing’s Wrong, But…
Let’s look at the outcome again.
class Memo::Render < Render[ engine: :json ] end puts Memo::Render.engine #=> :json puts Memo::Render.superclass #=> #<Class:0xf028>
It works! However, as you can see, Memo::Render
‘s superclass is not Render
, but the anonymous class we had to create just to transport the configuration from Render
to the subclasses.
The intermediate class is completely redundant, is never instantiated, and sits unnecessarily between Render
and Memo::Render
, polluting the ancestor chain and making it hard to debug since inspect
will show the “wrong” class.
Now, don’t get me wrong. Neither do I care about this class in terms of memory, nor do I want to further complicate Ruby for the sake of “my” extension. An additional class that could be avoided by simply passing through arguments is a useless class and must be reconsidered.
Passing Through Arguments
Having discussed the status-quo, here’s how this could work automatically by extending Ruby.
class Memo::Render < Render, engine: :json end
In this example, a new syntax allows to pass additional arguments to the inheritance. Don’t let the comma confuse you, this is just a suggestion.
Check the implementation to see why I am so convinced by this new pattern.
class Render singleton_class.attr_accessor :engine def self.inherited(subclass, engine: self.engine) super subclass.engine = engine end end
This is a lot less code, and much easier to understand. The key is: All arguments from the inheritance are directly passed through as additional arguments to inherited
– that’s it (line 4-7). No magic, no intermediate state, nothing.
Here’s the result.
class Memo::Render < Render, engine: :json end puts Memo::Render.engine #=> :json puts Memo::Render.superclass #=> Render
I hope this explains my idea better, and demonstrates that my only goal is a massive simplification, not a complicated language addition or hard-to-understand meta-programming. You don’t have to use the additional arguments if you don’t need them and you can still do your anonymous classes if your code requires that.
This extension can also be applied to included
and extended
and help “tuning” modules easier. Peace!
Let’s take a look to C++ templates with all pros and cons…
LikeLike