When I first started learning Rails back in the day, it was my first introduction to Ruby: I was learning them both at the same time. As a result, the line between them was rather blurry; I didn’t know what was coming from Ruby, and what was coming from Rails. The Rails approach of monkey-patching Ruby didn’t help. If I’m being honest, I didn’t realize that Object#blank? wasn’t a Ruby method until only a few years ago.

I think it’s really important to understand the software you’re using, particularly big frameworks like Rails that follow convention over configuration and thus end up magically doing things for you. What are you supposed to do when the magic is gone?

Today I want to talk about one such piece of magic: why you need to require things in Ruby, and why you (generally) don’t in Rails.

First of all, you’ll note that the title refers to Rails autoloading. I did that for the SEO, but that’s not the only piece in play, here. In fact, there are three:

  1. Ruby
  2. Bundler
  3. Rails

Let’s take each in turn. Note that for all my examples, I’m using version 3.0.1 of Ruby via RVM, with a gemset made particularly for this purpose.

$ rvm install 3.0.1
$ rvm use 3.0.1@demystify-autoloading --create

Ruby

Ruby does no hand-holding, and has no magic. In general, you must require everything. Let’s investigate this with a simple example.

Create a new directory for this example with two files in it, one of them executable:

$ mkdir ~/ruby-example
$ touch ~/ruby-example/foo.rb
$ touch ~/ruby-example/main.rb
$ chmod a+x ~/ruby-example/main.rb

Make ~/ruby-example/foo.rb look like this:

class Foo
  def hello
    puts 'hello, world!'
  end
end

This file defines a simple class with a single method that prints a message. Now let’s fill out ~/ruby-example/main.rb to use it:

#!/usr/bin/env ruby

foo = Foo.new
foo.hello

This simply instantiates our class and runs the method that should print our message. Let’s run it:

$ ~/ruby-example/main.rb
/home/kyrofa/ruby-example/main.rb:3:in `<main>': uninitialized constant Foo (NameError)

This doesn’t work because Ruby has no magic here: it has no idea what Foo is, despite the fact that the file defining it is sitting right next to the one you’re executing. You need to require it. Make ~/ruby-example/main.rb look like this:

#!/usr/bin/env ruby

require_relative 'foo'

foo = Foo.new
foo.hello

Run it again, and behold success:

$ ~/ruby-example/main.rb
hello, world!

In general you need to be pretty explicit in Ruby about your dependencies. In trade, it’s usually pretty easy to determine where your dependencies are coming from, since there’s an explicit require chain that pulls it in.

Bundler

One could make the case that needing to explicitly include everything you’re using is tedius. This is actually one of the roles that Bundler can play. Most folks know that Bundler can be used to manage your dependencies, but did you know that it can also be used to require those dependencies?

Create a new directory for this example with two files in it, one of them executable:

$ mkdir ~/bundler-example
$ touch ~/bundler-example/Gemfile
$ touch ~/bundler-example/main.rb
$ chmod a+x ~/bundler-example/main.rb

The ~/bundler-example/Gemfile is used by bundler to control your dependencies. Make it look like this:

source 'https://rubygems.org'
gem 'hello-world'

The hello-world gem is useful because it just prints a message when you require it. Install it:

$ cd ~/bundler-example/
$ bundle install

Now make ~/bundler-example/main.rb look like this:

#!/usr/bin/env ruby

require 'bundler/setup'
Bundler.require

That’s it. Run it:

$ ~/bundler-example/main.rb
hello world!
this is hello world library

That’s the message printed by the hello-world gem when you require it, as I mentioned above. That means Bundler included it for you, as a result of it being part of your Gemfile. You can hand Bundler.require different groups to limit what it requires for you, as well (production versus development or test, for example).

Now that you’re familiar with this technique, go to any Rails application and check out config/application.rb, and you’ll see something like this:

# ...

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

# ...

So Rails applications do this for you by default, which is why you generally don’t need to require any of your dependencies. There are caveats, however.

Lots of Rails developers (including me) have settled on a modular approach to developing Rails applications, which means that a given application is split across a number of gems, which are often Rails engines. If you’re also in this camp, one thing to keep in mind is that gems (including engines) do not have this functionality. Gems generally execute within the context of the main application’s Gemfile, so it can’t lean on Bundler like this. Gems are expected to require their own dependencies. That doesn’t mean you need to do it in every file, though, if that annoys you. Ruby’s require has global effect, which means once something is required in one place, it can be used everywhere. If you have a file which is generally loaded as part of using the gem (e.g. lib/<gem name>.rb), you can put your requires there and just use them everywhere.

Rails

So Bundler magic covers why you don’t need to require your gem dependencies, but what about other .rb files within the same app (or engine)? If you’re familiar with Rails, you may have noticed you generally don’t need to require those either. That flies in the face of our understanding of Ruby, and it’s thanks to Rails magic called autoloading.

There are other resources that describe Rails autoloading in detail. Those are recommended reading, so I won’t repeat them.

The general idea is this: if you access a constant that is missing in the current execution context (e.g. a class defined in another file), Rails will try to find and require the file that defines it for you by searching a pre-defined set of autoload paths following a convention where each namespace for the constant equals a directory. A simplistic example: if you try to access Bar and it’s not defined, Rails will quickly check to see if there’s a bar.rb directory in any of the autoload paths. If there’s not, it’ll either complain or you’ll get the standard Ruby error for trying to access an undefined thing. If you try to access MyModule::Bar, it’ll search those same autoload paths for my_module/bar.rb.

As a caveat, Rails engines don’t add their lib/ directory to the set of autoload paths by default, but you’ll notice engine developers often do it themselves for consistent behavior.

Conclusion

Rails has done a magnificent job of making it trivial to get up and running developing new web applications, thanks to its convention-over-configuration approach. Its use of Bundler and autoloading to handle requires is no small part of that overall philosophy, and I suspect it’s a large reason for its success. That said, if you never really try to understand what Rails is doing for you, you will eventually get bitten when it doesn’t do what you expect. Gaining at least a small insight into how this particular piece of the puzzle works will pay dividends down the road, and I hope this was helpful in accomplishing exactly that.