The amazing adventures of Doug Hughes

TIL about TracePoint!

Let’s say I have a Ruby class that is extended by many subclasses. I want each of these subclasses to have a constant named FRIENDLY_NAME. Obviously, I can edit each of these classes and add the constant. But, if it’s not specified in the subclass, is there a way to define the constant in the subclass from the superclass?

For example, maybe I could set each subclass’ FRIENDLY_NAME to be the subclass’ class name demodularized and underscored, when FRIENDLY_NAME is not already defined.

It turns out that the answer is yes!

To expand on this, I had a superclass I wanted to extend many times:

class BaseClass
end

I wanted all extended versions of the class to have a FRIENDLY_NAME constant. When specified explicitly in the subclass, that would be the value for the constant. EG:

class Subclass1  Subclass1::FRIENDLY_NAME
=> "Bob seems like a friendly name!"

However, maybe I have another subclass where I don’t define the constant. EG:

class Subclass2  Subclass2::FRIENDLY_NAME
NameError: uninitialized constant Subclass2::FRIENDLY_NAME
from /bundle/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/active_support.rb:79:in `block in load_missing_constant'
Caused by NameError: uninitialized constant Subclass2::FRIENDLY_NAME
from /bundle/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/active_support.rb:60:in `block in load_missing_constant'

How to add that constant? You can add self.inherited to the superclass. This method is called each time the superclass is extended. There’s also a method on Module named const_set, so I could automatically set the constant on the child like this:

class BaseClass
  def self.inherited(child)
    child.const_set(:FRIENDLY_NAME, child.name)
  end
end

This works just fine for Subclass2:

pry(main)> Subclass2::FRIENDLY_NAME
=> "Subclass2"

However, it produces ugly warnings for Subclass1

(pry):10: warning: already initialized constant Subclass1::FRIENDLY_NAME
(pry):3: warning: previous definition of FRIENDLY_NAME was here

Initially I tried using the const_defined? method to check to see if the FRIENDLY_NAME constant was already defined. The problem is, the self.inherited method is invoked before the constant is defined in the subclass. Basically, as soon as Ruby see < BaseClass it runs self.inherited.

Eventually I found this SO question about how to execute code after a class has been defined. The answer there was to use TracePoint.

TracePoint is a class that allows us to handle events in our code. These could be when a line of code is executed, when we call a method, when an error is raised, etc. What we care about is :end event, which happens when a class or module definition is finished.

Knowing that, we can update our self.inherited method like this:

class BaseClass
  def self.inherited(child)
    TracePoint.trace(:end) do |t|
      if child == t.self
        unless child.const_defined?(:FRIENDLY_NAME)
          child.const_set(:FRIENDLY_NAME, child.code)
        end
      end
    end
  end
end

Now, both Subclass1 and Subclass2 have a FRIENDLY_NAME constant, but neither produce warnings!

Having written all of the above, I’m probably not going to use it. I initially wrote this writeup to answer my own StackOverflow question, but as a commenter points out, this is basically reinventing inheritance for constants in a way that will be difficult for people who haven’t read this post to understand.

An easier way to do this is simply to create a class method like friendly_name. Override it in classes where I want to, and don’t in those where I do.

Comments on: "Defining Constants on a Subclass from a Superclass in Ruby" (1)

  1. Got it. Clear as the peel of a distant bell! Mom

    Sent from my iPhone

    Like

Comments are closed.

Tag Cloud

%d bloggers like this: