This post was published more than 4 years ago. It's likely the contents are obsolete or nostolgic. If you think the contents are worth updating, open an issue. Otherwise, sit back and enjoy the stroll down memory lane.
Classic Rails Autoloading and Singleton Classes
Rails constant autoloading is a really nifty feature.
It lets us reference constants without explicitly requiring them. It is also what allows Rails to pick up changes to files without having to constantly restart your server.
It’s one of those features that goes under appreciated. If it’s working you probably don’t even know it’s there – until something goes wrong
One gotcha I’ve run into a number of times is how it interacts with Ruby’s class << self
syntax.
10,000 Foot View
When you write Ruby code, you are likely defining lots of constants. Module names are constants, class names are constants, and of course constants are constants!
When you run the code, Ruby expects you to have defined the constants you use. Typically this is done by requiring the ruby file(s) that you plan to use.
# in foo.rb
class Foo
end
# in bar.rb
require "foo"
class Bar
def initialize
@foo = Foo.new
end
end
If we were to reference Foo
in bar.rb
without the require "foo"
, because const_get
fails, ruby calls const_missing
.
Without any further intervention, it would raise an… uninitialized constant error a la NameError (uninitialized constant Foo)
The const_missing
method serves as a hook for dynamically resolving the missing constant.
Classic Rails autoloading works by doing exactly this!
It implements const_missing
and relies on file location conventions to figure out where to find the missing constant.
What happens when you break convention you ask? Welp, let’s just say it might be a bit of a learning experience.
First some Ruby
class YourClass
def self.hello
"Why hello there"
end
class << self
def hello2
"Why hello there"
end
end
end
YourClass.hello # => Why hello there
YourClass.hello2 # => Why hello there
You may have come across this syntax in your ruby usage.
The class << self
syntax isn’t simply an alternate syntax for defining class methods.
While ultimately it results in a class method, you’re doing so by opening the class’s “eigenclass”.
Sounds intimidating at first, but eigen just means “self” in German.
Remember that in the ruby object model you have classes and instances of those classes. A user defined class is an instance of the class Class (whew ).
When inside an instance method, self
is a way to refer to the instance itself.
This usage of self
is different because we’re operating in the scope of the class instead of the instance.
As a result, in this example, self
is actually YourClass
.
Walking around saying the word “Eigenclass” sure does make you sound smart, but I find it’s easier to reason about when referred to as a “singleton class”.
What’s a Singleton Class?
If every class you define is an instance of Class
, where do your class’s class methods actually live?
One option would be to define them as instance methods on the class Class
.
I think you’d be forgiven for thinking that given the “class” vs “instance of class” distinction discussed above.
While in a way poetic, we wouldn’t want Ruby to define our class methods as instance methods on the class Class
because it would mean all instances of Class
would have our class method.
Class.new.hello #=> Why hello there
Class.new.hello2 #=> Why hello there
That would be… insanity.
To alleviate this, each ruby class has an anonymous singleton class that it uses to store class methods. In other words, your class methods are actually defining instance methods on this singleton class. Similarly, when you call your class method, your class calls an instance method on the class’s singleton class.
You can actually see your class’s singleton class by doing YourClass.singleton_class
!
# should be the same list
YourClass.singleton_class.instance_methods(false)
YourClass.singleton_methods
As the name implies, you cannot (thank goodness) create instances of the singleton class:
YourClass.singleton_class.new #=> NOPE
So What’s the Difference?
When you use the def self.
approach, ruby takes care of defining an instance method on the singleton class for you.
You’ll notice the class << self
variant doesn’t use def self.
at all.
Hopefully now you see why — because the class << self
syntax opens up the singleton class allowing you to define instance methods on it directly.
As a result of doing this, your class now has access to those methods as class methods!
Probably the most common reason I see folks reaching for class << self
is to take advantage of this instance method definition as a way to define private class methods without resorting to the admittedly awkward private_class_method
method.
The other difference that’s seems less talked about is the impact to Module.nesting
which is crucial to any autoloading implementation.
Because you’re defining methods in different scopes (YourClass
vs YourClass.singleton_class
), you’re going to get different answers when Module.nesting
is called.
class A
class << self
def foo
Module.nesting
end
end
def self.bar
Module.nesting
end
def baz
Module.nesting
end
end
A.foo # => [#<Class:A>, A]
A.bar # => [A]
A.new.baz # => [A]
Back to Rails
In Rails 5, here is roughly how the “classic” autoloading algorithm works (taken from the autoloading guides):
if the class or module in which C is missing is Object
let ns = ''
else
let M = the class or module in which C is missing
if M is anonymous
let ns = ''
else
let ns = M.name
end
end
loop do
# Look for a regular file.
for dir in autoload_paths
if the file "#{dir}/#{ns.underscore}/c.rb" exists
load/require "#{dir}/#{ns.underscore}/c.rb"
if C is now defined
return
else
raise LoadError
end
end
end
# Look for an automatic module.
for dir in autoload_paths
if the directory "#{dir}/#{ns.underscore}/c" exists
if ns is an empty string
let C = Module.new in Object and return
else
let C = Module.new in ns.constantize and return
end
end
end
if ns is empty
# We reached the top-level without finding the constant.
raise NameError
else
if C exists in any of the parent namespaces
# Qualified constants heuristic.
raise NameError
else
# Try again in the parent namespace.
let ns = the parent namespace of ns and retry
end
end
end
The part we’re going to focus on is the condition that says:
if M is anonymous
let ns = ''
From the section above we discovered the class’s singleton class is an anonymous class. Because of this, this condition is going to expect unloaded constants explicitly defined in our singleton class to be located in the top level namespace.
So let’s go to an example you might see in the wild:
module SomeNamespace
class PolicyService
def self.create_policy
RatingService.create
end
class << self
def create_policy2
RatingService.create
end
end
end
end
Variant 1: def self.
(class scope)
As defined, the nesting inside PolicyService.create_policy
is:
# [
# SomeNamespace::PolicyService
# SomeNamespace
# ]
As a result, PolicyService.create_policy
works as expected, first checking for RatingService
in SomeNamespace::PolicyService
, then SomeNamespace
and finally at the top level via ::RatingService
.
Variant 2: class << self
(singleton class scope)
Subtly different, the nesting for PolicyService.create_policy2
is:
# [
# #<Class:SomeNamespace::PolicyService>,
# SomeNamespace::PolicyService,
# SomeNamespace
# ]
If the constant is already loaded by something else, great, no autoloading required.
However, if the constant is missing, rails is going to look at the top level namespace.
If the constant isn’t defined at the top level namespace, you will get a NameError
.
Worse yet, if there is a constant defined at the top level namespace, it might not be the desired constant!
This can be a real pain in the neck to track down since you are unlikely to always load classes in the same order when running tests. Similarly, in development, your classes will get reloaded to reflect the changes you’ve made, potentially causing them to be reloaded in a new order.
Sound familiar?
Conclusion
I’d recommend not using class << self
when you’re working in a Rails autoloaded directory (e.g app/**/*
).
If you’re using private class methods so much that you feel the need to crack open the singleton class, perhaps there’s an instance hiding in your code waiting to be discovered.
Hopefully next time you see a missing constant error, you’ll be able to track it down faster with this knowledge in your toolbox.
—–
Consider this post a farewell letter to our dear friend (and occasional mortal enemy), Classic Rails Autoloading.
Rails 6 reworks (thankfully ) how autoloading is done using a library called Zeitwerk and I’m really excited for it!
See a mistake? Kindly let me know by filing an issue