1

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 a constant in a subclass from a 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.

Doug Hughes
  • 931
  • 1
  • 9
  • 21
  • FWIW, I can use something like `child.const_set(:FRIENDLY_NAME, 'foo')` from `self.inherited` in the superclass, but that gives me a warning that I'm redefining a constant, even though I haven't yet defined it. "warning: already initialized constant" – Doug Hughes Jun 04 '20 at 11:54
  • Correction: The warning was coming from a class where I _did_ set the `FRIENDLY_NAME` constant. The problem is that `self.extended` in the super class is executed before the constant is defined in the subclass and there I'm setting the constant if it doesn't already exist. Thus, the warning is actually coming from the subclass when I'm trying to set its real value. :/ – Doug Hughes Jun 04 '20 at 12:00
  • 2
    This is interesting, but not a good case for constants. You're effectively jumping through hoops to try and get this to behave how inheritance of methods already works, so I would use class level methods which are dead simple, understood by pretty much all Ruby devs, and which require no special knowledge of lesser known Ruby features like `TracePoint`: https://gist.github.com/meagar/0f8379ec706949594e76f9c91c7d6ec1 – user229044 Jun 04 '20 at 12:28
  • Honestly, you're right and a class method is exactly what I'll be doing. Still, like you said, it's interesting. – Doug Hughes Jun 04 '20 at 13:04

1 Answers1

4

TIL about TracePoint!

To expand on my original problem, 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 < BaseClass
  FRIENDLY_NAME = 'Bob seems like a friendly name!'
end

Thus:

pry(main)> 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 < BaseClass
end

This is what I'd get without the constant (obviously):

pry(main)> 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!

Doug Hughes
  • 931
  • 1
  • 9
  • 21