In Progress
Unit 1, Lesson 1
In Progress

Logical Require

Video transcript & code

In episode #240, we met the require_relative command, and saw how loading files using paths relative to the current file could clean up our code. But we ended on an ominous note, saying that require_relative comes with serious drawbacks as well. Today we're going to investigate those drawbacks.

We'll start off with the biggest problem: sometimes require_relative just flat-out doesn't work. Let's look at an example.

In episode #240, we had a project containing a helper module, a sinatra application, and a script file in a bin directory.

$ tree myapp
myapp
├── bin
│   └── hello
├── cow_helpers.rb
└── web.rb

The sinatra app file uses require_relative to load the helper module.

require "sinatra"
require_relative "cow_helpers"

get "/" do
  text = params["text"] || "Hello"
  content_type "text/plain"
  CowHelpers.moo(text)
end

Let's flip over to a scratch buffer and try to load up the web.rb file. But instead of using require or load to accomplish this, we'll do it by first reading in the file and then evaluating it.

code = File.read("myapp/web.rb")
eval code
# ~> -:2:in `eval': cannot infer basepath (LoadError)
# ~>    from (eval):2:in `<main>'
# ~>    from -:2:in `eval'
# ~>    from -:2:in `<main>'

Unfortunately, we get an error when doing this: "cannot infer basepath". This is because require_relative needs to know what file it is being evaluated in in order to figure out relative paths. Since the code is now being evaluated without any context, require_relative fails.

We can fix this by including an explicit path for the file being evaluated in the arguments to eval.

code = File.read("myapp/web.rb")
eval code, binding, File.expand_path("myapp/web.rb")

"OK" you're probably thinking. "This is a really weird way to load a file, AND it's trivially fixed, so how is this a real problem?"

Well, back in Ruby 1.9.3 this wasn't so trivially fixed. And in fact the Rack framework, upon which just about all Ruby web frameworks are based, loaded up its "rackup files" in exactly this weird way. As a result, it was impossible to use require_relative in a rackup file, a fact that surprised me on more than one occasion.

require_relative "web"

run Sinatra::Application

And of course prior to Ruby 1.9.2, require_relative didn't even exist. Which is obviously a problem if you're trying to maintain backwards compatibility in an application or gem.

Fortunately, the "cannot infer basepath" issue seems to have been addressed in more recent versions of Ruby. So if backward compatibility is not a concern, this may not present a barrier to using require_relative.

Let's move on now to another problem. Let's say our app has expanded to contain multiple helper modules, all in a helpers subdirectory. In addition to cow_helpers we now have auth_helpers and comment_helpers. And the web.rb file loads all of them, using require_relative.

require "sinatra"
require_relative "helpers/cow_helpers"
require_relative "helpers/auth_helpers"
require_relative "helpers/comment_helpers"

get "/" do
  text = params["text"] || "Hello"
  content_type "text/plain"
  CowHelpers.moo(text)
end

In addition, let's imagine that we are breaking our project down into microservices, and we have a new apps directory to contain them. So we move the web.rb file to this subdirectory.

$ mkdir apps
$ mv web.rb apps

We have now broken our web frontend. How? Well, all of our requires are relative. Since we've moved web.rb to a new subdirectory, all of those relative paths are now wrong. We have to update each of them in order to accommodate the move.

require "sinatra"
require_relative "../cow_helpers"
require_relative "../auth_helpers"
require_relative "../comment_helpers"

get "/" do
  text = params["text"] || "Hello"
  content_type "text/plain"
  CowHelpers.moo(text)
end

We've effectively coupled the content of our web.rb file to its external position in the project file hierarchy. This doesn't seem like a good idea.

In order to fix this, we need to switch to requiring logical paths to features. A logical path is a path like we are used to using to load standard library or gem code: it simply contains the name of the feature to be required, possibly namespaced with a directory, but with no absolute or relative guidance about where to find the corresponding file.

require "sinatra"
require "cow_helpers"
require "auth_helpers"
require "comment_helpers"

get "/" do
  text = params["text"] || "Hello"
  content_type "text/plain"
  CowHelpers.moo(text)
end

The next question is how do we make this work. The answer depends on the type of project. For a Rails application, the needed load path modifications are generally handled for us. For some other type of app, we may need to modify the load path ourselves. Sometime very early in the loading of the app, we can prepend the project's root path onto the $LOAD_PATH using unshift and File.expand_path.

$LOAD_PATH.unshift(File.expand_path("../..", __FILE__))
require "sinatra"
require "cow_helpers"
require "auth_helpers"
require "comment_helpers"

get "/" do
  text = params["text"] || "Hello"
  content_type "text/plain"
  CowHelpers.moo(text)
end

I usually find that I have multiple places in a project where I need to configure the load path. For instance, besides for the app startup file, I may also need the load paths to be set up in a Rakefile, in a test helper file, and in utility script files.

For this reason I usually construct a separate environment.rb file, which does basic setup such as initializing the load path. Then I use require_relative or require with expand_path to load just the environment.rb file. It's true that this means I haven't gotten away from relative paths completely, but at least I know I'll only have to deal with a maximum of one relative require in a file. All the other requires will use logical paths.

$LOAD_PATH.unshift(File.expand_path("..", __FILE__))
#+BEGIN_SRC ruby
require_relative "../environment"
require "sinatra"
require "cow_helpers"
require "llama_helpers"
require "coatimundi_helpers"

get "/" do
  text = params["text"] || "Hello"
  content_type "text/plain"
  CowHelpers.moo(text)
end

Setting up a gem to use logical require paths internally is even easier. Since RubyGems automatically adds the gem's lib directories to the load path, in that case we don't need to mess around with the load path at all.

Using logical paths is really just an extension of the same general philosophy that we apply to classes and methods: we try to avoid coupling the internals of an entity to the structure of its surroundings. And we separate the implementation of those entities from knowledge about how to find them.

So now you are acquainted with the darker side of require_relative. While it may seem quite useful at first, in most cases it's best to avoid relative requires as much as possible. Instead rely on the load path to enable loading features from logical paths. Happy hacking!

Responses