Loading files in Rails

This post assumes that you’ve already read Part 1 - Loading Files In Ruby . If you haven’t go follow that link and go and read it, then come back here.

So we’ve gone through how Ruby loads files. But we’re still trying to work out how Rails does its magic trick of knowing about all your classes.

# app/controllers/invoice_controller.rb

class InvoiceController < ApplicationController # 🪄🌈❓❗❓
  # some business logic
end

I want to introduce you to two characters, let’s call them Classic and Zeitwerk. Here’s a picture of them:

The Autoloaders


As you can probably tell, they’re very different characters.

  • Zeitwerk is organised, proactive and never makes mistakes. He’s a joy to work with (although some people think he’s a bit fussy).

  • Classic is the complete opposite. He does everything last minute, makes a lot of mistakes and is always one step away from being fired.

In this post, we’re going to look at the way that Rails used to load files (Classic), then we’re going to look at the new way (Zeitwerk).

How autoloading used to work

Back in the day, Rails got Classic to do all the autoloading.

Classic’s approach for this was to read through all the code, and then when he hit a constant he hadn’t seen before, he quickly tried to find a file which contained that constant. For this, he relied pretty heavily on usingObject.const_missing.

I’m sorry, what’s missing?

const_missing is a cool ruby meta-programming trick that allows you to determine what happens when a constant is referenced that doesn’t exist. Normally, Ruby would raise a NameError if you refer to a non-existent constant.

But we can change that…

class Object
  def self.const_missing(c)
    puts "there is no such constant #{c}!"
  end
end

BANANA_NAME => "there is no such constant BANANA_NAME!"
APPLE_NAME => "there is no such constant APPLE_NAME!"
GRAPE_NAME => "there is no such constant GRAPE_NAME!"

Here, we’re re-opening the Object class and overriding const_missing. Now, instead of raising of raising an error, it prints a message to the user.

Where things really get interesting is where we use const_missing in combination with const_set. Object.const_set allows you to define (or redefine) any constant. So with these two methods working together, we can dynamically set a constant if we find that it doesn’t exist yet. 🤯

Imagine a customer walking into a shop and asking for something that the shop doesn’t have in stock. Then imagine the shop owner going into the back and quickly making the item on the spot. They bring it out and the customer buys it.

That’s basically what we’re doing here.

Inventive Shopkeeper

"I've nearly found them. Just give me one more minute and I'll definitely have found them"


Here’s an example of this sort of thing:

class Object
  def self.const_missing(constant)
    # Turn `BANANA_NAME` into "Banana"
    fruit_name = constant.to_s.split('_').first.capitalize  
    new_value = "#{["Tim", "Ann", "Bob", "Ada"].sample} the #{fruit_name}" 
    
    # Sets the value of the constant (for any future references)
    const_set(constant, new_value)
    
    # Now we return the new value of `BANANA_NAME`
    new_value
  end
end

BANANA_NAME => "Tim the Banana"
APPLE_NAME => "Ann the Apple"
GRAPE_NAME => "Bob the Grape"

This is the trick that Rails (and Classic) relied on for many years.

So how did this work in Rails?

When Ruby hits ApplicationController, it tries to find this class using its Constant Lookup Hierarchy.

If Ruby hasn’t seen the constant before, then it will go all the way through the Constant Lookup Hierarchy, and find nothing. Ruby’s next step is to trigger Object.const_missing. This effectively hands the task of finding this class over to Rails, because Classic has overridden const_missing.

Ruby passing the baton

"I know where I might be able to find that constant" thinks the Rails wizard


In Classic’s version of Object.const_missing , it would convert the class name to snake_case then try to load a file of that name.

Here’s a rough idea of how that worked:

class Object
  def self.const_missing(constant)
    # translates `ApplicationController` to `application_controller`
    underscored_version_of_constant_name = constant.to_s.underscore
    
    # here we `require` the file `application_controller`, thus loading 
    # the missing constant `ApplicationController`
    require constant.to_s.underscore 
    
    # now we fetch (and return) the - no longer missing! - `
    # ApplicationController` constant
    Object.const_get(constant) 
  end
end
Important caveat! The code above is just a rough example of how Rails used to do it. The actual classic autoloader version of `const_missing` was much more complex and coped with a bunch of edge-cases.


What was wrong with how Classic did things?

This approach worked for many years, but it came with some problems.

I said earlier that Classic made a lot of mistakes. You can see a long list of these mistakes in the Rails Guide for version 5.2 (the last version that still exclusively used the Classic autoloader).

Here’s an example of one of them, imagine the code below:

# date.rb
class Date
  def initialize
    puts "Would you like to go on a date with me ❤️?"
  end
end
# supermarket/date.rb
module SuperMarket
  class Date
    def initialize
      puts "Would you like to buy one of these tasty dates? 🌴"
    end
  end
end
# supermarket/dried_fruits.rb
module Supermarket
  class DriedFruits
    def initialize
      @date = Date.new
    end
  end
end

So we have a couple of Date classes, which we definitely don’t want to mix up (🌴 != ❤️)

If we first call Supermarket::DriedFruits.new, before any references are made to either of the Date classes, everything works as expected:

> Supermarket::DriedFruits.new
Would you like to buy one of these tasty dates 🌴?

Ruby doesn’t know about either of the Date classes, so we trigger const_missing. The Classic autoloader does its thing and loads the correct class.

But! If you first referenced one of the top-level (romantic) dates, then we get different behaviour.

> Date.new
Would you like to go on a date with me ❤️?


> Supermarket::DriedFruits.new
Would you like to go on a date with me ❤️?

This happens because Classic autoloading is only triggered if Ruby is unable to first find a constant of that name in it’s constant lookup hierarchy. In the above example, Ruby looks in the following places:

Supermarket::DriedFruit::Date # nope 
Supermarket::Date # nope - `supermarket/date.rb` has not been loaded yet!
::Date # aha! I found a Date class!

It already knows about a Date class, so we never get to const_missing and supermarket/date.rb doesn’t get loaded. 😞

Having the value of constants depend on the order that are referenced is some pretty crazy behaviour.

So the old autoloader was a bit of a hack. It also completely ignored the perfectly nice autoload functionality that already existed in Ruby (as we discussed in Part 1…). You’d think if you were going to implement an autoloader you would use autoload - right?

And, as we’ll see, that’s exactly how Zeitwerk likes to do things.

So how does zeitwerk do it?

Zeitwerk doesn’t wait to find a constant that is has not seen before. Zeitwerk is proactive.

Before looking at any of the application code, Zeitwek first scans all the autoload paths when the app loads.

Ruby passing the baton

Zeitwerk scanning all your application's autoload paths


It then creates a bunch of autoload statements, based on the files it has seen, and assumptions about what the classes in those files are likely to be called.

# Zeitwerk sees:
`application_controller.rb`
# Zeitwerk assumes this class exists:
=> ApplicationController
# So Zeitwerk autoloads:
autoload(:ApplicationController, 'application_controller.rb')

# Zeitwerk sees:
`supermarket/date.rb`
# Zeitwerk assumes this class exists:
=> Supermarketr::Date
# So Zeitwerk autoloads:
Supermarket.autoload(:Date, 'supermarket/date.rb')

So to go back to our earlier example, this is Ruby’s new search through the Constant Lookup Hierarchy.

Supermarket::DriedFruit::Date # nope 
Supermarket::Date # I have an autoload for that! Let's load the file!

By using Ruby’s in-built autoload functionality, rather than hacking around it, we bypass all the problems we saw with the Classic autoloader.

By creating these autoload statements when your app first boots, the required classes can then be loaded while Ruby is searching the Constant Lookup Hierarchy, rather than waiting for this search to finish

So, the new autoloader is much better than the old one, and everyone is happy about it. 🎉🌈

In 2021, the Rails wizard finally fired Classic (Rails 7 no longer supports the classic autoloader), and the long list of autoloading “Gotchas” was removed from the Rails guides.

One small problem

However, remember I mentioned before that Zeitwerk was a bit fussy? It’s not his fault really, it’s just the way he likes to work.

You see, while Classic found the class name, then guessed the file name:

# Classic
"ClassName".underscore => "class_name"

Zeitwerk finds the file name and then guesses the class name:

# Zeitwerk
"class_name".camelize => "ClassName"

But actually, it’s harder to guess class names from file names than the other way round, because of Rails conventions about file and class naming. Files are always follow the snake_case format, eg api.rb. However, the class name in this file could be either API or Api.

It’s not a big problem. Zeitwerk can handle either situation, you just need to let him know 🙂.

There are a few other things to be aware of if you’re upgrading a Rails app that currently uses the classic autoloader, which you can find here.

Still hungry for more autoloading?

If you want to keep learning about how Rails loads files, below is a great talk from Xavier Nora (who created zeitwerk) about how he did it.

Get more Rails Explained

Subscribe to get all the latest posts by email.

    We won't send you spam. Unsubscribe at any time.