Better Know A Ruby Thing: Method Lookup
Notes and Corrections
Before we get fully started here, a couple of notes on Better Know Singleton Classes, which, among other things, got mentioned on Hacker News, giving me comments there for the first time in years, maybe for the first time ever.
One Hacker News comment suggested that “Eigenclass” was coined by _why the lucky stiff as a joke and was then adopted by the community. I looked this up and that doesn’t seem to be the case… _why’s Poignant Guide To Ruby uses “metaclass” when it discusses this feature (at least the version I have does…). Matz used “eigenclass” in the O’Reilly Ruby book. (You didn’t ask, but version 1 of the Pickaxe uses “singleton class”, per https://ruby-doc.com/docs/ProgrammingRuby/).
It’s also worth mentioning that both “eigenclass” and “metaclass” are used in the Ruby source, but to mean slightly different things. I did not stare at the code long enough to figure out exactly how they are different. For example, this is a comment in module.h
.
* - eigenclass: = singleton class
* - metaclass: class of a class.
Metaclass is a kind of singleton class.
I think I can get this to make sense if I stare at it hard enough, but I am not at all sure.
One or two people (including https://bsky.app/profile/ufuk.dev) correctly noted that my description of how singleton classes work in Ruby is, at best, simplified, and at worst, inaccurate. Basically, my description is consistent with what Ruby’s API allows you to determine about classes, but is not a reflection of how Ruby works internally. Ruby’s internals are closer to Smalltalk than I let on, we’ll talk more about that in a second, because it’s important to method lookup.
I haven’t had this inserted commercial message in the last few emails, because I fear that it is annoying, but then we picked up a handful of new readers from the Hacker News thing so… quick tour, bear with me.
- I’m the co-author of Programming Ruby 3.3 (The Pickaxe Book) which is available in ebook from Pragmatic Press and physically from Amazon among others. I’d love it if you purchased a copy.
- If you’d like to support this newsletter, you can sign up for a Monthly Subscription for $3/month or an Annual Subscription for $30/year. Subscription money goes toward paying for Buttondown. Paid subscribers (very) occasionally get extras.
Thanks!
Method Lookup In Ruby
And with that, let’s talk about method lookup in Ruby, which is from one perspective really clean and simple, and from another angle, kind of messy.
This is part of what the official Ruby documentation says about method lookup:
Here is the order of method lookup
for the receiver's class or module `R`:
- The prepended modules of `R` in reverse order
- For a matching method in `R`
- The included modules of `R` in reverse order
If `R` is a class with a superclass,
this is repeated with `R`'s superclass until a method is found.
What’s striking about this is it is:
- not fully accurate, because it doesn’t include the singleton class
- overly confusing, because there’s a simpler way to talk about the lookup order.
To be fair, this is how I talked about method lookup in Ruby for a long time. The way I’d describe now it is:
Given a receiver object, r
,
- Ruby takes the list of classes and modules that you can see defined in
r.singleton_class.ancestors
. This list contains all the classes and modules inr
's ancestor tree. - Ruby looks at the first element in the ancestors list and if it has the method, that’s called. If not, it goes to the next element and so on..
Note: Ruby does not literally call this ancestors
method – I don’t think – but this is conceptually the list that Ruby uses for lookup.
For example…
module Pre
def say = "prefix"
end
class Kla
prepend Pre
def say = "class"
end
r = Kla.new
r.singleton_class.ancestors
Results in:
[<Class<Kla:0x0000000122d165f0>>,
Pre,
Kla,
Object,
JSON::Ext::Generator::GeneratorMethods::Object,
PP::ObjectMixin,
Kernel,
BasicObject]
The point here is if I were to call the method r.say
, Ruby will first look in the singleton class, where it will not find a say
method, and then look in the prepended module, Pre
, where it will find the method and it will execute that call. Ruby will then stop, so the fact that the method is also defined in the class Kla
is not used.
If we then write this code:
def r.say = "banana"
Then the singleton class does have a say
method and r.say
now returns banana
and stops lookup at that point.
So, how does one get on the ancestor list?
The official Ruby documentation for method lookup effectively describes this process of creating the ancestor list.
Conceptually, the ancestor list looks like this:
- Start with the inheritance tree, from the class to its superclass and so on to
Object
and thenBasicObject
. - For each class in that list, any modules that are added using
prepend
are inserted in the ancestor list before the class that prepends them. If the class prepends more then one module, a second prepended class is added before the first prepended class, and so on. The result is that modules that are prepended later supersede modules prepended earlier. - For each class in the list, any modules that are added using
include
are inserted after the class that includes them. Again, modules included later are inserted earlier, so that the last module included wins. - This process is recursive, if an included module also has includes and prepends, those modules also go on the tree relative to that module.
You can see these behaviors in our example. The Kla
class prepends the Pre
module, so Pre
is in the ancestor tree before Kla
. Later, JSON::Ext::Generator::GeneratorMethods::Object
,
PP::ObjectMixin
, and Kernel
are all included by Object
. (Technically, the JSON and PP modules are, I think, monkey patched in when the JSON
and PP
gems are auto-included, Kernel
and Object
also are a little non-standard in their actual internals, for performance reasons).
But I thought we said something about singleton classes?
We did. Or rather, I did.
What I said in the past blog post, and actually, the way I was taught Ruby, was that Ruby objects effectively have a pointer to their singleton class, and that method lookup is sort of special-cased to start at the singleton class.
That’s, like, true-ish, in the sense that following that model will allow you to accurately predict Ruby’s behavior, but it’s not how the internals work. Even what I said above about method lookup being based on the ancestor list of obj.singleton_class
isn’t exactly how the internals work, though it’s (I think) as close as you can get given Ruby’s public API.
What Ruby actually does, and thanks to Ufuk Kayserilioglu for this, is that Ruby makes the singleton class the “Class” of the instance, and then makes the actual class the superclass of the singleton class. Ruby hides this from you in the public library methods – if you say "thing".class
, you get String
, not thing’s singleton class. I think – I think – that the only way you can really see the actual inheritance tree is by asking the singleton class for its ancestors list.
Why does it matter how singleton classes work? Well, integrating singleton classes into the ancestor list makes method look up much more elegant. You don’t have to do weird contortions to talk about included and prepended modules the way the official docs do. It’s just a simple walk up the ancestor list, all the extra modules and singleton classes are rolled up into that list and all you need to do is go through the list one by one. The complication is in creating that ancestor list, not in traversing it.
What if the method isn’t found?
That’s usually bad.
If you get all the way up to BasicObject
and nobody has matched the method name, then the method is… wait for it… missing. I did a whole newsletter about method_missing, so we’re not going to go over that whole thing.
The point here is how Ruby behaves, and what Ruby does is start the search all over again from the singleton class of the original receiver object, but this time rather than searching for the method name, it searches for method_missing
. If some class or module in the chain implements method_missing
, it’s executed. Eventually you get to BasicObject
, which definitely implements method_missing
– specifically it throws a NameError
.
That’s super
Most Object-Oriented languages let you execute a superclass method with the keyword super
or something similar, and Ruby is not an exception. Ruby has keywords for when the parser needs to do something unusual, and super
looks like a method call but doesn’t quite behave like a method call.
A call to super
is evaluated as “take the name of the method the super call is in, call that method, but start one level up on the ancestors
list”. In other words, super
doesn’t only follow the inheritance hierarchy, it follows the full ancestors
list with modules that have been added using include
or prepend
.
There are a couple of fun side-effects of that definition.
Calling super
from a prepended module will almost always take you back to the original method. This means you can create wrappers to insert when debugging…
If you want to trace an unreachable method in a class you don’t own – Fill in your own name instead of unreachable_method
:
module Debug
def unreachable_method(*args, **kwargs)
p "calling unreachable method with #{args} #{kwargs}"
puts caller[0..3]
super
end
end
class ClassIDontOwn
prepend Debug
end
I’m prepending a module to capture calls, but then using super
to continue along the normal track. Calling super
is a technique to use with method_missing
, as well – handle the method name if you can, continue normal processing with super
. (Before prepend
you could do something similar by aliasing a method, but that was more complex and confusing).
The way that super
handles arguments is what makes it a keyword.
Invoking super
with no arguments continues the call stack implicitly passing the same arguments that were passed to the original method. But, you can override this – if you explicitly pass arguments to super
, then those arguments are what’s passed to the next method in the list. This is a useful feature in the not-unheard-of case where a subclass adds arguments to initialize
that are not in the parent class:
class Parent
def initialize(name)
@name = name
end
end
class Child < Parent
def initialize(name, age)
@age = age
super(name)
end
end
In this case, the Child
class takes an extra argument, but passes just the first argument back to the Parent
class to get its functionality.
Which gets us to a legitimate piece of trivia, the only (I think) place in Ruby where empty parentheses are syntacticly meaningful is in a call to super
. If you want to explicitly pass no arguments to the parent class, the call needs to be super()
.
class Thing
def value
25
end
end
class LittleThing < Thing
def value(expensive = true)
50 if expensive
super()
end
end
The parent class method takes no arguments so the subclass needs to explicitly pass no arguments to the parent in order to use super
.
Did you know refinements are a thing?
Yes, they are. Refinements are a way of customizing Ruby’s behavior locally – it’s a monkey patch but it’s only applicable where explicitly mixed in. Refinements change class method lookup, but not for the class that incudes the refinement, but for the class referenced by the refinement. Without going into a 1500 word digression for a feature I’ve used exactly once since it was added to Ruby ten years ago…
If a refinement is active for the receiving class or module, then the refinement is effectively placed in the ancestor list before the class or module and before anything prepended by the class or module. I don’t really know how this works internally and I have to save something for Future Noel to do if we ever decide to Better Know refinements, but effectively anything in the refinement goes after the singleton class and before anything that is actually connected to the class. If, for some reason, the refinement prepends or includes modules, those are placed relative to the refinement exactly as they would be for a class or module.
Hey, I was wondering… can a singleton class have a singleton class
Yes. We will not be investigating this further.