There are many ways to load code and access file-related constants in Ruby. We can create a clear architecture by separating and handling concerns into classes and pulling in only the classes we depend on.
Many full-stack frameworks like Rails and Hanami offer a built-in method to access the classes we want, as long as we stick with a certain convention. How does this work?
In this post, we will explore three different options for loading code: using load
, require
, and autoload
. We will also look into the Ruby gem Zeitwerk
. Zeitwerk
is the default code-loading mechanism for many new projects, and a lot of established projects are switching to this great gem. Rails and Hanami both utilize Zeitwerk
as their code-loading tool.
Before we dive into the code loading options on offer in Ruby, let's take a quick look at $LOAD_PATH
to get to grips with how code loading works.
$LOAD_PATH
in Ruby
Before we dive deeper into this topic, let's summarize the $LOAD_PATH
in Ruby, as it is essential in understanding how loading works.
The global variable $LOAD_PATH
or shorter $:
references all directories with Ruby source files registered in an array. So when you prepend bundle exec
with your program name, Ruby adds all the program gems to the load path.
Then, when we load or require a file, Ruby iterates over this array and searches for the file name. This can add significant time to the booting process, as we might have to search for a given file in many places on the hard disk.
Keeping that in mind, we'll now explore some Ruby code-loading options in detail.
Options for Loading Code in Ruby
We have three main options for loading code:
- Using the
load
method. This loads and parses a Ruby program in a specified file every time you call the method. - The
require
method. With this, we load and parse a given file only once. - The third option is
autoload
. We declare upfront that Ruby should require a specified file when we use a constant that doesn't exist yet.
load
a File in Ruby
As mentioned, with load
we parse and execute a Ruby file.
If the filename we set as an argument is a relative path, i.e., it starts with ./
or ../
, the file will be loaded relative to the current working directory.
Remember: This doesn't mean it's relative to the file where the load is located — it's instead relative to the file that started the Ruby process. If you want to know what the working directory is, run Dir.pwd
. Otherwise, Ruby searches for a file with the given name in the $LOAD_PATH
.
Let's see how load works:
# foo.rb
$x = 4 # define global Variable
class Foo
def bar
puts "I'm doing it now"
end
end
Then we load the file with load "foo.rb"
.
What happens when we load this file? The global variable $x
is created and set to the integer 4. The constant Foo
is also created, and we can instantiate an object from it. So far, this seems just like the more familiar require
call.
But what happens when we load
this file again? If we have changed our global value $x
, it resets to 4.
In older Ruby versions, we also might get a warning that the constant Foo
is being redefined.
Another problem with load
is that every reload takes some time to run as the Ruby VM is eval
-ing the file. require
, on the other hand, loads and parses a file just once, during the first run.
Wrapping the File with load
Right now, every time we load or require a file, we pollute the global namespace. To avoid this, we can use the load
method with the wrap parameter set to true
. This will create an anonymous module where all the constants and methods defined in a loaded file are placed. Here is an example:
# file to be loaded foo.rb
class Foo
puts self.name # => Here we will see where our Foo class is located in
def bar
puts "I'm doing it now"
end
end
###############################
# Now in another file run this:
load "foo.rb", true
If you run this, Ruby will output something like this: #<Module:0x00007fa3430682e8>::Foo
.
Why is this useful? For one thing, we are not polluting the global namespace. But since Ruby creates an anonymous module, we can only access the stuff we load with some trickery.
Nevertheless, wrapping the file is still useful if we want to configure something in our application or set some things up. This way, we can achieve what we want without leaving anything behind.
Accessing the Wrapped File Constants
There are two ways to access the constants we create in the wrapped file. The first and most obvious is to set a module name as the argument for the wrap parameter.
class Foo
def bar
puts "I'm doing it now"
end
end
###############################
# Now in another file run this:
module Parent
end
load "./code_loading/file.rb", Parent
Parent::Foo.new
Here is how we can have access to the anonymous Ruby module:
class Foo
def bar
puts "I'm doing it now"
end
end
throw :wrapper, Module.nesting.last
############################################
# When we load the file we catch the error
# and make the module accessible
mod = catch :wrapper do
load "./code_loading/file.rb", true
end
mod::Foo.new
There is a hidden problem with wrap
. If the author of the loaded file does something like this, it will break the sandbox:
class ::Foo
# stuff
end
Now Foo
is in the global namespace, and there is no way to prevent this pollution.
Ruby Code Loading with require
Just like the load
method, require
is a method in the Kernel
module. The big difference between these two methods is how they behave on multiple calls.
While load
always reevaluates a file, require
doesn't. Its response is also different. If you call require
for the first time and the method actually loads something, it returns true
, otherwise, it returns false
. load
, on the other hand, always returns true
.
Some pseudo-code for require
would look something like this:
def require(file_path)
if !already_loaded_files? file_path
eval(File.read(find_file_in_load_path(file_path)))
mark_file_as_loaded! file_path
true
else
false
end
end
require
accepts absolute paths for files as well as just a simple name. If we need a simple file, require
also searches in the $LOAD_PATH
. This, of course, means that we still have the same disadvantages we had with load
.
Simpler File Searching with require_relative
require_relative
always looks for a file that is relative to the current file you are working on. require_relative
doesn't iterate over a directory, and no real search is involved. Ruby looks into the path specified and tries to load the file. If it isn't there, we get a LoadError
.
Unfortunately, as of now, require
doesn't have a wrap
parameter like load
does, so require
always pollutes the global namespace. But there has been some recent discussion on the Ruby issue tracker to add something like wrap
to the require
method.
autoload
Code Loading in Ruby
Our third option for code loading in Ruby is autoload
. With autoload
, we tell the Ruby VM upfront what constants might be accessed and where to find and load them.
A big advantage of this approach is that we only load the files we really need. Therefore, the booting process can be really fast as we just pay for what we use. When we actually need the constant, autoload
requires the file with the method require
.
Here's how we set up an autoload:
### lib/services/foo.rb
module Services
class Foo
def bar
puts "bar"
end
end
end
### lib/services.rb
module Services
autoload :Foo, "#{__dir__}/services/foo.rb"
# Now we have access to the Foo constant in this module
# and we will load the file when we actually need it.
# Foo.new.bar => loads the file and prints "bar"
end
The most popular gem for managing loading files is Zeitwerk
, and it works with autoload
. Now let's see how Zeitwerk works and how to set it up.
Code Loading with Zeitwerk
Zeitwerk takes a directory and makes every file underneath it available to load. The convention is that every new sub-directory is a new module, and every file defines a class with the same name as the file.
Zeitwerk offers an option for code reloading that is useful during development. Rails uses this mode in an active development environment.
How does it work under the hood? Here is some very, very simplified pseudo-code:
module Zeitwerk
class Loader
def initialize()
@root_dir = []
end
def push_dir(dir)
@root_dirs << dir
end
def setup
# scan root dirs and define autoloads for each file
@root_dirs.each do |dir|
Dir.foreach(dir) do |file|
next if file == "." || file == ".."
autoload file.class_name.to_sym, File.expand_path(file, dir)
end
end
end
end
end
loader = Zeitwerk::Loader.new
loader.push_dir(__dir__)
loader.setup
Now, with the last call to loader.setup
, we get access to every constant defined in one of the files relative to the current file.
Of course, this simplified code doesn't highlight many of the other great features that Zeitwerk
has, like eager loading and handling namespaces. That's outside of the scope of this post, and I'll leave it to you to dig deeper and explore all Zeitwerk
has to offer.
How to Set Up Zeitwerk
with Roda
Since Rails and Hanami use Zeitwerk
as their default, let's see how we can configure Zeitwerk
for Roda:
### in config.ru
loader = Zeitwerk::Loader.new
loader.push_dir(__dir__)
loader.push_dir("#{__dir__}/lib")
if ENV['RACK_ENV'] == 'production'
loader.setup
Zeitwerk::Loader.eager_load_all
run App
else
loader.enable_reloading
loader.setup
run ->(env) {
loader.reload
App.call(env)
}
end
If we configure our app like this, we have code reloading during development and eager loading in production. This way, the boot time during development is fast, as only the files that are immediately needed are loaded. In production, we load everything upfront and have a faster response time.
Wrapping Up
In this post, we first took a quick look at $LOAD_PATH
in Ruby to lay the foundations of how code loading works. We then explored three options to load code in some detail: load
, require
, and autoload
(which works well with the Zeitwerk
gem).
So, how should we load our code? Most of the time, we should use require
and Zeitwerk
to automate that. If you have special requirements and want to put loaded code into a namespace, the load
method with a wrap parameter is a good option.
Happy code loading!
P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!
Top comments (0)