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.
$ 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
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,
We can fix this by including an explicit path for the file being evaluated in the arguments to
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
comment_helpers. And the
web.rb file loads all of them, using
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.
#+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!