Code Loaders in Ruby - Understanding Zeitwerk
With Zeitwerk, you can streamline your programming knowing that classes and modules are available everywhere.
What are Code Loaders?
Code loaders let developers define classes and modules across different files and folders and use them throughout the codebase without explicitly requiring them. Rails is a good example of a piece of software that uses code loaders. Programming in Rails doesn't require explicit require calls to load models before using them in controllers. In fact, in Rails 6, everything in the app directory is auto-loaded on app boot, with a few exceptions.
While it is easy to think code loading is all about making calls to require, it isn't that simple. Code loading can be further broken down into three parts, as follows.
- Auto Loading: This means code is loaded on-the-fly as required. For example, in Rails, running
rails sdoesn't load all the models, controllers, etc. But, on the first hit of the modelUser, it runs the auto loading mechanism to find and use the model. This is auto loading in action. This has some advantages for our development environment, as we have faster app andrails consolestartup times.Rails.config.autoload_pathcontrols the paths to be auto-loaded. - Eager Loading: This means code is loaded into memory at app startup and doesn't wait for the constants to be called before requiring it. In Rails, code is eager loaded in production. From the explanation above, autoloading code in production will result in slow response times, as each constant will be required on-the-fly.
Rails.config.eager_load_pathscontrols the paths to be eager loaded. - Reloading: The code loader is constantly watching for changes to files in the
autoload_pathand reloads files when it notices any changes. In Rails, this can be quite useful in development, as it enables us to runrails sand simultaneously make changes without needing to restart the rails server. This is reloading in action.
We can easily see that most of these concepts have been developed and live in Rails. Zeitwerk changes this! Zeitwerk enables us to bring all the code loading action to any Ruby project.
What is Zeitwerk?
Zeitwerk is an efficient and thread-safe code loader for Ruby and can be used in any Ruby project, including Web frameworks (Rails, Hanami, Sinatra), Cli tools, and gems. With it, you can streamline your programming knowing that classes and modules are available everywhere. Traditionally, Rails and some other gems have built-in code loaders to enable this functionality. However, Zeitwerk extracts these concepts into a gem and allows Rubyists to apply these concepts to their projects.
Installing Zeitwerk
First things first, we need to install the gem:
gem install zeitwerk
# OR in your Gemfile
gem 'zeitwerk', '~> 2.4.0'
Configuring Zeitwerk
So let's start with the basics:
require 'zeitwerk'
loader = Zeitwerk::Loader.new
...
loader.setup
The above code instantiates a loader instance and calls setup. After the call to setup, loaders are ready to load code. But, before that, all the necessary configurations on the loader should be covered already. In this article, I'll cover a few of the configurations on the loader and conventions for structuring your code.
- File Structure: For Zeitwerk to work, files and directory names need to match the modules and class names they define. For example,
lib/my_gem.rb -> MyGem
lib/my_gem/foo.rb -> MyGem::Foo
lib/my_gem/bar_baz.rb -> MyGem::BarBaz
lib/my_gem/woo/zoo.rb -> MyGem::Woo::Zoo
- Root Namespaces: Root Namespaces are directories where
Zeitwerkcan find your code. Whenmodulesandclassesare referenced, Zeitwerk knows to search the root namespaces with the matching file name. For example,
require 'zeitwerk'
loader = Zeitwerk::Loader.new
loader.push_dir("app/models")
loader.push_dir("app/controllers")
// matches as follows
app/models/user.rb -> User
app/controllers/admin/users_controller.rb -> Admin::UsersController
There are two primary ways to define the root namespace for two different use cases. The default way is shown below:
// init.rb
require 'zeitwerk'
loader = Zeitwerk::Loader.new
loader.push_dir("#{__dir__}/bar")
...
loader.setup
// bar/foo.rb
class Foo; end
This means the class Foo can be referenced without an explicit Bar::Foo, as the bar directory acts as a root namespace. The second way to define a namespace is to explicitly state the namespace in the call to push_dir:
// init.rb
require 'zeitwerk'
module Bar
end
loader = Zeitwerk::Loader.new
loader.push_dir("#{__dir__}/src", namespace: Bar)
loader.setup
// src/foo.rb
class Bar::Foo; end
There are a few things to note from in this code:
- The module
Barwas already defined before being used bypush_dir. If the module we want to use is defined by a third-party, then a simple require will define it before we use it in our call topush_dir. - The
push_direxplicitly specifies the namespaceBar. - The file
src/foo.rbdefinedBar::Foo, notFoo, and did not need to create the directory, likesrc/bar/foo.rb.
Independent code loader: By design, Zeitwerk allows each project or app dependency to manage its individual project tree. This means the code loading mechanism of each dependency is managed by that dependency. For example, in Rails 6, Zeitwerk handles code loading for the Rails app and allows each gem dependency to manage its own project tree separately. It is an error condition to have overlapping files between multiple code loaders.
Autoloading: With the above setup, once the call to
setupis made, all classes and modules will be available on demand.Reloading: To enable reloading, the
loaderhas to be explicitly configured for it. For example,
loader = Zeitwerk::Loader.new
...
loader.enable_reloading # you need to opt-in before setup
loader.setup
...
loader.reload
The loader.reload call reloads the project tree on-the-fly, and any new changes are visible immediately. However, we still need a surrounding mechanism to detect changes to the file system and call loader.reload. A simple version is shown below:
require 'filewatcher'
loader = Zeitwerk::Loader.new
...
loader.enable_reloading
loader.setup
...
my_filewatcher = Filewatcher.new('lib/')
Thread.new(my_filewatcher) {|fw| fw.watch {|filename| loader.reload } }
Using Zeitwerk in Rails
Zeitwerk is enabled by default in Rails 6.0. However, you can opt-out of it and use the Rails classic code loader.
# config/application.rb
config.load_defaults "6.0"
config.autoloader = :classic
Using Zeitwerk in Gems
Zeitwerk provides a convenient method for gems, as long as they use the standard gem structure (lib/special_gem). This convenience method can be used as follows:
# lib/special_gem.rb
require 'zeitwerk'
module SpecialGem
end
loader = Zeitwerk::Loader.for_gem
loader.setup
With the standard gem structure, the for_gem call adds the lib directory as a root namespace, enabling every code in the lib directory to be found automatically.
For more inspiration, you can check out gems using Zeitwerk:
By
Molly Struve, Sr. Site Reliability Engineer, Netflix