DEV Community

Cover image for How to Load Code in Ruby
Gernot Gradwohl for AppSignal

Posted on • Originally published at blog.appsignal.com

How to Load Code in Ruby

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)