Dear Ruby (2): The Functional Inherited Hook

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.

inherit

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!

One thought on “Dear Ruby (2): The Functional Inherited Hook

Leave a comment