I decided to follow up on my email about requires vs. autoload in Ruby. I thought I'd do more research to figure out how to best manage requiring code in a Ruby gem. I decided to take a look at a few popular Ruby projects to get inspired.
The results are... not inspiring.
Code: https://github.com/bundler/bundler/tree/060e2f953c8f08dd7bed9711863e6341eebe5fe2
Ruby's dependency management system.
- Uses autoload.
- Weird circular-dependency:
lib/bundler/ui.rbautoloadslib/bundler/ui/rg_proxy.rbbut the latter require the former. - Unnecessary requires:
lib/bundler/gem_tasks.rbrequiresbundler/gem_helpereven though autoload forGemHelperis already inlib/bundler.rb.- Both
lib/bundler.rbandlib/bundler/rubygems_ext.rb(and a bunch of others) requirepathname.
- Requires
bundlerinspec_helper.rbbut also sometimes requires particular files in some specs.
Code: https://github.com/teamcapybara/capybara/tree/03092d5c7f3673d68548ee6e36ea1978449ecac5
Popular framework for feature tests.
- Used to use autoload but removed for thread safety in https://github.com/teamcapybara/capybara/commit/c3e75f8988f640d3587bffbc48ead011ef2665d3
- Many files required in
lib/capybara.rbbut then also many required in other files. Unclear how it's decided which requires go where from the first glance. - Requires
capybarainspec_helper.rbbut also sometimes requires particular files in some specs.
Code: https://github.com/jekyll/jekyll/tree/76a0fc38887e684f65237dd35861a54dc4ab97e2
Popular static website generator.
- Uses autoload for most of its code, but there are some exceptions where
requireis used (why?). - Namespacing doesn't match file paths (e.g.
Jekyll::CollectionReaderis inlib/jekyll/readers/collection_reader.rb). - Some unnecessary requires but not too bad:
setrequired inlib/jekyll.rbandlib/jekyll/cleaner.rb.
- Requires
jekyllintest/helper.rbbut sometimes requires individual files too in particular tests (why?).
Code: https://github.com/pry/pry/tree/cebad88656f5c092ce01c30c6c0a279cef68ca8f
Popular REPL, replacement for IRB.
- Does not use autoload.
- Sometimes has a file to load all classes in a module (e.g.
lib/pry/helpers.rb), sometimes not (lib/pry/command.rbdefinesPry::Commandthat is both a class and a namespace, but constants namespaced withPry::Command::are not loaded in that file). - Inconsistencies between modules and paths, e.g. files in
lib/pry/commandshave classes namespaced withPry::Command::(singular). - Some third-party gems required in
lib/pry.rb, some in other files. - Unnecessary requires:
coderayrequired inlib/pry.rband one more place.
- Requires
pry/testableinspec/helper.rbwhich in turn requirespry. Does not require individual files in tests.
Code: https://github.com/rack/rack/tree/ab008307cbb805585449145966989d5274fbe1e4
Webserver interface that most Ruby web frameworks use.
- Uses autoload. Oddly, some non-top-level autoloads are defined in
lib/rack.rbbut some in separate files. However, file paths match namespaces in all cases. - Rack has no third-party dependencies so we don't see any requires for other gems.
- No stdlib requires in
lib/rack.rb. It seems each file tries to be explicit about its stdlib dependencies. - Unnecessary requires:
- Even though autoloads for
FileandUtilsare defined inlib/rack.rb,lib/rack/file.rbandlib/rack/utils.rbare explicitly required in other files.
- Even though autoloads for
- Requires whatever it wants in tests.
Code: https://github.com/bbatsov/rubocop/tree/6fb5a84976567549302881de4923e21819040756
The most popular linter for Ruby.
- Does not use autoload.
- Uses a lot of
require_relative, not sure why. - Almost completely flat require structure,
lib/rubocop.rbhas over 500 lines of requires. - Some third-party gems required in
lib/rubocop.rb, some in other files. - Unnecessary requires:
lib/rubocop.rbrequiressetbut many cops also require it.
- Requires
rubocopand a bunch of things inspec_helper.rbbut then requires more things in individual spec files.
I can't help but feel that Ruby's require/autoload system is broken if even the most popular and mature projects can't get it right. The biggest problem seems to be global scope where everything is dumped when it's required. This makes enforcing correct requires per file almost impossible or at least impractical (you could run each spec file in a separate rspec process, but who does that?).
Therefore, the simplest approach seems to be to use as few requires/autoloads in as few places as possible and for any interaction with the code (specs, external use) simply require the whole gem.
Autoload can be beneficial because it frees you from figuring the right order of loading files which can be cumbersome if you don't do explicit requires per file (you can just list everything alphabetically), but has its limitations too. Sometimes, you don't want to load things lazily, you need to be sure they are always loaded.