In Progress
Unit 1, Lesson 1
In Progress

Load

Video transcript & code

One of the things I like to do on this show is to take fundamental Ruby topics and dig into them a bit deeper than you might have seen before. In this vein, today and for the next few episodes I though we might talk about the way we load code in Ruby.

Loading up new code in Ruby is a fundamentally dynamic, and somewhat messy, process. At its core, it all boils down to evaluating code live. What do I mean by this? Well, consider the following script. The first few lines define a method, effectively loading new code into the running system. The last line invokes that code. From the VM's perspective, there is no difference, no distinction between loading up code and executing it. It's all just code execution. Some of that execution just happens to define methods, modules, or classes as a side effect.

def hello(name)
  puts "Hello, #{name}"
end

hello("Mike")
# >> Hello, Mike

We can even write code to generate a string, and then cause that string to be read and evaluated as code.

def hello(name)
  puts "Hello, #{name}"
end

code = '
  def goodbye(name)
   puts "Goodbye, #{name}"
  end
'

eval code

goodbye("Crow")
# >> Goodbye, Crow

Note that unlike languages such as Java or C++, there is no concept of class loaders or compilation units or packages that need to be created to encapsulate code before it is loaded. Code is simply read in and executed on the fly.

Of course, for any meaningfully-sized program, we're going to want to break our program down into multiple files. Let's say we've moved our greeter code into a file called greet.rb. Let's also pretend that Ruby has no built-in methods for loading code. We can still easily load the code. All we need to do is read the code in using File.read, and then evaluate it. Then we can invoke the loaded method.

def hello(name)
  puts "Hello, #{name}"
end
code = File.read("./greet.rb")
eval code

hello("Servo")
# >> Hello, Servo

This code is simple, but it has some limitations. First off, it's a lot of typing for something we're going to do again and again. But an even bigger deal is that it only works with either absolute file paths, or paths relative to the current working directory. Absolute paths aren't portable. And relative paths are problematic since there's no way we can guarantee that a program will always be executed from the same directory.

To load code more portably and robustly, we need some kind of search path of places to look for code, the same way the UNIX shell's PATH variable lets us execute commands without knowing where they live in the filesystem. Of course, Ruby has just such a search path; it's a global variable named $LOAD_PATH. When we dump the path, we can see it is pre-loaded with a number of default system directories.

Some of these directories are a little anachronistic. For instance, the site_ruby and vendor_ruby directory trees were more commonly used in the days before the concept of "Ruby Gems" existed, as a place for locally-installed Ruby libraries to be found. Nowadays locally-installed code is more likely to be found in separate gem directories.

But we can also see, at the very end of the $LOAD_PATH, some very important directories that are home to all of Ruby's standard libraries. There are two of these: one for Ruby files, and one for compiled binary libraries. We'll talk more about this distinction later on.

$LOAD_PATH
# => ["/home/avdi/.rubies/ruby-2.1.2/lib/ruby/site_ruby/2.1.0",
#     "/home/avdi/.rubies/ruby-2.1.2/lib/ruby/site_ruby/2.1.0/x86_64-linux",
#     "/home/avdi/.rubies/ruby-2.1.2/lib/ruby/site_ruby",
#     "/home/avdi/.rubies/ruby-2.1.2/lib/ruby/vendor_ruby/2.1.0",
#     "/home/avdi/.rubies/ruby-2.1.2/lib/ruby/vendor_ruby/2.1.0/x86_64-linux",
#     "/home/avdi/.rubies/ruby-2.1.2/lib/ruby/vendor_ruby",
#     "/home/avdi/.rubies/ruby-2.1.2/lib/ruby/2.1.0",
#     "/home/avdi/.rubies/ruby-2.1.2/lib/ruby/2.1.0/x86_64-linux"]

If we want to search this $LOAD_PATH for files to load, we're going to need to get a bit fancier. First, let's define a helper method named #try_load_file. It will take a filename as a parameter. It first tests to see if the filename refers to an actual file in the filesystem. If so, it lets us know what file it is loading, and then reads in the file and evaluates it the same as we did before. Then it returns true. If the file is not found, it just returns false.

Now let's define a method named #load_lib. It will take a filename as an argument. First it tries to load the given name as-is, returning immediately if that succeeds. If not, it cycles through the load path, appending the filename to each directory and trying to load the resulting expanded path.

If it makes it past this loop, that means the requested library file was not found anywhere. In that case, we signal an error.

def try_load_file(file)
  if File.file?(file)
    puts "LOAD #{file}"
    eval File.read(file)
    return true
  else
    return false
  end
end

def load_lib(name)
  return true if try_load_file(name)

  $LOAD_PATH.each do |dir|
    file = File.join(dir, name)
    return if try_load_file(file)
  end

  fail LoadError, "Library not found: #{name}"
end

Let's try this code out. First, let's try loading up our greet.rb file.

eval File.read("./loader.rb")

load_lib("greet.rb")

hello("Gypsy")
# >> LOAD greet.rb
# >> Hello, Gypsy

As we can see, it finds and loads the file in the current directory. Let's now try and load up a Ruby standard library. We'll go with the pstore library, which we talked about in episode #162. When we try to load pstore.rb, we see that it was successfully located in the Ruby standard library directory.

eval File.read("./loader.rb")

load_lib("pstore.rb")
# >> LOAD /home/avdi/.rubies/ruby-2.1.2/lib/ruby/2.1.0/pstore.rb

So we can load code in the current directory, and code in system directories. But what about portably loading up our own library files regardless of the current directory? For instance, what if we change the current working directory to the filesystem root? Loading our greet.rb file no longer works.

eval File.read("./loader.rb")

Dir.chdir "/"

load_lib("greet.rb")

# ~> (eval):36:in `load_lib': Library not found: greet.rb (LoadError)
# ~>    from -:5:in `<main>'

To make this work, we need to modify the $LOAD_PATH. Let's first grab the library directory by finding the current file, and then expanding the path one level up from the file. Now we need to add the file to the $LOAD_PATH. We could just append it to the end of the $LOAD_PATH array, but this is a bad habit to get into. The $LOAD_PATH is searched from beginning to end, and it is nearly always the case that, when there are two libraries with the same name, we want our own version to be found rather than the system one. So it is conventional to prepend directories to $LOAD_PATH, rather than appending them. In order to prepend to an array, we have to use the rather unfortunately-named #unshift method.

If we dump out the value of $LOAD_PATH, we can see that our code directory is now sitting at the beginning of it.

Now if we change working directory and try to load our greet.rb file, it is successfully found.

eval File.read("./loader.rb")

my_lib_dir = File.expand_path("..", __FILE__)
# => "/home/avdi/Dropbox/rubytapas/235-require"

# $LOAD_PATH << my_lib_dir
$LOAD_PATH.unshift(my_lib_dir)

$LOAD_PATH
# => ["/home/avdi/Dropbox/rubytapas/235-require",
#     "/home/avdi/.rubies/ruby-2.1.2/lib/ruby/site_ruby/2.1.0",
#     "/home/avdi/.rubies/ruby-2.1.2/lib/ruby/site_ruby/2.1.0/x86_64-linux",
#     "/home/avdi/.rubies/ruby-2.1.2/lib/ruby/site_ruby",
#     "/home/avdi/.rubies/ruby-2.1.2/lib/ruby/vendor_ruby/2.1.0",
#     "/home/avdi/.rubies/ruby-2.1.2/lib/ruby/vendor_ruby/2.1.0/x86_64-linux",
#     "/home/avdi/.rubies/ruby-2.1.2/lib/ruby/vendor_ruby",
#     "/home/avdi/.rubies/ruby-2.1.2/lib/ruby/2.1.0",
#     "/home/avdi/.rubies/ruby-2.1.2/lib/ruby/2.1.0/x86_64-linux"]

Dir.chdir "/"

load_lib("greet.rb")
# >> LOAD /home/avdi/Dropbox/rubytapas/235-require/greet.rb

There is more to be said about loading files. But we're getting close to the five minute mark, so I'm going to save the rest for the next episode. Happy hacking!

Responses