Skip to content

Commit 4c99825

Browse files
authored
Merge pull request #7 from tonywok/ts-rails-autoloading
Extract Rails classic autoloading post
2 parents 4d8f195 + 3cfd888 commit 4c99825

File tree

2 files changed

+300
-2
lines changed

2 files changed

+300
-2
lines changed
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
---
2+
layout: post
3+
author: Tony Schneider
4+
title : Classic Rails Autoloading and Singleton Classes
5+
date : 2020-01-23
6+
tags : software
7+
---
8+
9+
Rails constant autoloading is a really nifty feature.
10+
11+
It lets us reference constants without explicitly requiring them.
12+
It is also what allows Rails to pick up changes to files without having to constantly restart your server.
13+
14+
It's one of those features that goes under appreciated.
15+
If it's working you probably don't even know it's there -- until something goes wrong :laughing:
16+
17+
One gotcha I've run into a number of times is how it interacts with Ruby's `class << self` syntax.
18+
19+
## 10,000 Foot View
20+
21+
When you write Ruby code, you are likely defining lots of constants.
22+
Module names are constants, class names are constants, and of course constants are constants!
23+
24+
When you run the code, Ruby expects you to have defined the constants you use.
25+
Typically this is done by requiring the ruby file(s) that you plan to use.
26+
27+
```ruby
28+
# in foo.rb
29+
class Foo
30+
end
31+
32+
# in bar.rb
33+
require "foo"
34+
class Bar
35+
def initialize
36+
@foo = Foo.new
37+
end
38+
end
39+
```
40+
41+
If we were to reference `Foo` in `bar.rb` without the `require "foo"`, because `const_get` fails, ruby calls `const_missing`.
42+
Without any further intervention, it would raise an... uninitialized constant error a la `NameError (uninitialized constant Foo)`
43+
44+
The `const_missing` method serves as a hook for dynamically resolving the missing constant.
45+
46+
Classic Rails autoloading works by doing exactly this!
47+
It implements `const_missing` and relies on file location conventions to figure out where to find the missing constant.
48+
49+
What happens when you break convention you ask?
50+
Welp, let’s just say it might be a bit of a learning experience.
51+
52+
## First some Ruby
53+
54+
```ruby
55+
class YourClass
56+
def self.hello
57+
"Why hello there"
58+
end
59+
60+
class << self
61+
def hello2
62+
"Why hello there"
63+
end
64+
end
65+
end
66+
67+
YourClass.hello # => Why hello there
68+
YourClass.hello2 # => Why hello there
69+
```
70+
71+
You may have come across this syntax in your ruby usage.
72+
73+
The `class << self` syntax isn’t simply an alternate syntax for defining class methods.
74+
While ultimately it results in a class method, you’re doing so by opening the class’s "eigenclass".
75+
Sounds intimidating at first, but _eigen_ just means "self" in German.
76+
77+
Remember that in the ruby object model you have classes and instances of those classes.
78+
A user defined class is an instance of the class _Class_ (whew :sweat_smile:).
79+
80+
When inside an instance method, `self` is a way to refer to the instance _itself_.
81+
This usage of `self` is different because we’re operating in the scope of the class instead of the instance.
82+
As a result, in this example, `self` is actually `YourClass`.
83+
84+
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".
85+
86+
### What's a Singleton Class?
87+
88+
If every class you define is an instance of `Class`, where do your class's class methods actually live?
89+
90+
One option would be to define them as instance methods on the class `Class`.
91+
I think you'd be forgiven for thinking that given the "class" vs "instance of class" distinction discussed above.
92+
93+
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.
94+
95+
```ruby
96+
Class.new.hello #=> Why hello there
97+
Class.new.hello2 #=> Why hello there
98+
```
99+
100+
That would be... insanity.
101+
102+
To alleviate this, each ruby class has an anonymous singleton class that it uses to store class methods.
103+
In other words, your class methods are actually defining instance methods on this singleton class.
104+
Similarly, when you call your class method, your class calls an instance method on the class’s singleton class.
105+
106+
You can actually see your class’s singleton class by doing `YourClass.singleton_class`!
107+
108+
```ruby
109+
# should be the same list
110+
YourClass.singleton_class.instance_methods(false)
111+
YourClass.singleton_methods
112+
```
113+
114+
As the name implies, you cannot (thank goodness) create instances of the singleton class:
115+
116+
```ruby
117+
YourClass.singleton_class.new #=> NOPE
118+
```
119+
120+
## So What’s the Difference?
121+
122+
When you use the `def self.` approach, ruby takes care of defining an instance method on the singleton class for you.
123+
124+
You’ll notice the `class << self` variant doesn’t use `def self.` at all.
125+
126+
Hopefully now you see why — because the `class << self` syntax opens up the singleton class allowing you to define instance methods on it directly.
127+
As a result of doing this, your class now has access to those methods as class methods!
128+
129+
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.
130+
131+
The other difference that’s seems less talked about is the impact to `Module.nesting` which is crucial to any autoloading implementation.
132+
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.
133+
134+
```ruby
135+
class A
136+
class << self
137+
def foo
138+
Module.nesting
139+
end
140+
end
141+
142+
def self.bar
143+
Module.nesting
144+
end
145+
146+
def baz
147+
Module.nesting
148+
end
149+
end
150+
151+
A.foo # => [#<Class:A>, A]
152+
A.bar # => [A]
153+
A.new.baz # => [A]
154+
```
155+
156+
## Back to Rails
157+
158+
In Rails 5, here is roughly how the "classic" autoloading algorithm works (taken from the autoloading guides):
159+
160+
```
161+
if the class or module in which C is missing is Object
162+
let ns = ''
163+
else
164+
let M = the class or module in which C is missing
165+
166+
if M is anonymous
167+
let ns = ''
168+
else
169+
let ns = M.name
170+
end
171+
end
172+
173+
loop do
174+
# Look for a regular file.
175+
for dir in autoload_paths
176+
if the file "#{dir}/#{ns.underscore}/c.rb" exists
177+
load/require "#{dir}/#{ns.underscore}/c.rb"
178+
179+
if C is now defined
180+
return
181+
else
182+
raise LoadError
183+
end
184+
end
185+
end
186+
187+
# Look for an automatic module.
188+
for dir in autoload_paths
189+
if the directory "#{dir}/#{ns.underscore}/c" exists
190+
if ns is an empty string
191+
let C = Module.new in Object and return
192+
else
193+
let C = Module.new in ns.constantize and return
194+
end
195+
end
196+
end
197+
198+
if ns is empty
199+
# We reached the top-level without finding the constant.
200+
raise NameError
201+
else
202+
if C exists in any of the parent namespaces
203+
# Qualified constants heuristic.
204+
raise NameError
205+
else
206+
# Try again in the parent namespace.
207+
let ns = the parent namespace of ns and retry
208+
end
209+
end
210+
end
211+
```
212+
213+
The part we're going to focus on is the condition that says:
214+
215+
```
216+
if M is anonymous
217+
let ns = ''
218+
```
219+
220+
From the section above we discovered the class's singleton class is an anonymous class.
221+
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.
222+
223+
So let's go to an example you might see in the wild:
224+
225+
```ruby
226+
module SomeNamespace
227+
class PolicyService
228+
229+
def self.create_policy
230+
RatingService.create
231+
end
232+
233+
class << self
234+
def create_policy2
235+
RatingService.create
236+
end
237+
end
238+
end
239+
end
240+
```
241+
242+
### Variant 1: `def self.` (class scope)
243+
244+
As defined, the nesting inside `PolicyService.create_policy` is:
245+
246+
```ruby
247+
# [
248+
# SomeNamespace::PolicyService
249+
# SomeNamespace
250+
# ]
251+
```
252+
253+
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`.
254+
255+
## Variant 2: `class << self` (singleton class scope)
256+
257+
Subtly different, the nesting for `PolicyService.create_policy2` is:
258+
259+
```ruby
260+
# [
261+
# #<Class:SomeNamespace::PolicyService>,
262+
# SomeNamespace::PolicyService,
263+
# SomeNamespace
264+
# ]
265+
```
266+
267+
If the constant is already loaded by something else, great, no autoloading required.
268+
269+
However, if the constant is missing, rails is going to look at the top level namespace.
270+
If the constant isn't defined at the top level namespace, you will get a `NameError`.
271+
272+
Worse yet, if there is a constant defined at the top level namespace, it might not be the desired constant! :scream:
273+
274+
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.
275+
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.
276+
277+
Sound familiar? :smiling_imp:
278+
279+
## Conclusion
280+
281+
I'd recommend **not** using `class << self` when you're working in a Rails autoloaded directory (e.g `app/**/*`).
282+
283+
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.
284+
285+
Hopefully next time you see a missing constant error, you’ll be able to track it down faster with this knowledge in your toolbox.
286+
287+
—--
288+
289+
Consider this post a farewell letter to our dear friend (and occasional mortal enemy), Classic Rails Autoloading.
290+
291+
Rails 6 reworks (thankfully :sweat_smile:) how autoloading is done using a library called [Zeitwerk](https://github.com/fxn/zeitwerk) and I'm really excited for it!

_site/index.html

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ <h2>
5757
</a>
5858
</li>
5959
<li>
60+
<span class="post-meta mono">2020-01-23</span>
61+
<a href="/2020/01/23/classic-rails-autoloading-and-singleton-classes.html">
62+
Classic Rails Autoloading and Singleton Classes
63+
</a>
64+
</li>
65+
<li>
6066
<span class="post-meta mono">2019-01-25</span>
6167
<a href="/2019/01/25/simple-code.html">
6268
Simple Code
@@ -129,7 +135,7 @@ <h2>
129135
</div>
130136
</div>
131137
</main><footer class="site-footer h-card">
132-
<data class="u-url u-uid" rel="me" href="/"></data>
138+
<a class="u-url u-uid" rel="me" href="https://webhaus.tonywok.com" hidden></a>
133139

134140
<div class="wrapper">
135141

@@ -156,6 +162,7 @@ <h2 class="footer-heading">
156162
</div>
157163
</div>
158164
</div>
159-
</footer></body>
165+
</footer>
166+
</body>
160167

161168
</html>

0 commit comments

Comments
 (0)