Loading classes from strings in Ruby

When working with Ruby, every once in a while you'll find yourself messing with a bunch of strings which are the names of classes. Given these strings, you'll need to go and instantiate the appropriate classes - something like taking "Array", "File" or "Booga" and figuring out how to call Array.new, File.new or... well, you get the picture. If you're wondering when you'd ever need this, just try instantiating controllers at runtime based on the urls being requested.

The usual suspect - Module#const_get

The first time I needed to do this, I did a Google search without actually thinking about it too much. The number one result was Module.const_get which does pretty much what is needed.
irb(main):005:0> Module.const_get('Array').new
=> []
As the docs would tell you, this returns the named constant which matches the string, which is pretty much what classes are - named constants which are instances of the Class class. Well, it worked, and beyond reading that this wouldn't work for classes which are nested in modules (const_get doesn't know or care about parsing stuff like the :: in Net::HTTP), I didn't bother too much. I did, however, come across something called Kernel#qualified_const_get which gets around this limitation, but more on that later.

But Module#const_get is a hack

Using const_get is effectively a hack - it uses the fact that class names are also constants to allow you to get hold of them.
Why do I say it's a hack? Well, if for some obscure reason you decided to create a class which was not a named constant, then you can't get hold of it with const_get. Here's an example:
ooga = Class.new   # Create a class the hard way
ooga.class_eval do # Add a method to it the hard way
  def hello
    "Hello!"
  end
end

p ooga.new.hello   # Prove that ooga can be instantiated
p Module.const_get('Array').new # As can 'Array', a named constant, using const_get
p Module.const_get('ooga').new.hello # But not 'ooga', which isn't
Output:
"Hello!"
[]
temp05.rb:10:in `const_get': wrong constant name ooga (NameError)
from temp05.rb:10


But what about eval?

Today, Srihari asked me if he could load up classes using eval. I said, 'Just use Module.const_get', but of course we were curious so we tried using eval and it worked. Obviously, given that eval effectively allows you to interpret code at runtime, it also handles nested classes and/or modules. Here's a code sample showing a regular invocation, an invocation using eval and an invocation using const_get (which fails) of a nested class:
puts "Ruby #{RUBY_VERSION}, #{RUBY_RELEASE_DATE}, #{RUBY_PLATFORM}"

module Ooga
  class Booga
    def hello
      "hello!"
    end
  end
end

puts Ooga::Booga.new

puts eval('Ooga::Booga').new

puts Module.const_get('Ooga::Booga').new
Output:
temp04.rb:15:in `const_get': wrong constant name Ooga::Booga (NameError)
from temp04.rb:15
Ruby 1.8.6, 2007-06-07, i486-linux
#<Ooga::Booga:0xb7c84774>
#<Ooga::Booga:0xb7c8465c>


Here Booga is a class nested in the module Ooga and as you can see const_get fails to fetch it because it isn't a constant (heck, it isn't even the right syntax for a constant).

Performance of const_get and eval

Let's take a look at the performance of const_get versus eval, an important factor if you're doing this inside a loop or some such.
puts "Ruby #{RUBY_VERSION}, #{RUBY_RELEASE_DATE}, #{RUBY_PLATFORM}"
require 'benchmark'

n = 1000000

Benchmark.bmbm(10) do |rpt|
rpt.report("simple invocation") do
  n.times {Array.new}
end

rpt.report("const_get invocation") do
  n.times {Kernel.const_get('Array').new}
end


rpt.report("eval invocation") do
  n.times {eval('Array').new}
end
end
Output:

Ruby 1.8.6, 2007-06-07, i486-linux
Rehearsal --------------------------------------------------------
simple invocation      0.910000   0.090000   1.000000 (  1.013326)
const_get invocation   1.400000   0.110000   1.510000 (  1.513216)
eval invocation        3.480000   0.220000   3.700000 (  3.692692)
----------------------------------------------- total: 6.210000sec

                         user     system      total        real
simple invocation      0.890000   0.100000   0.990000 (  1.000915)
const_get invocation   1.440000   0.080000   1.520000 (  1.514948)
eval invocation        3.490000   0.200000   3.690000 (  3.689998)

const_get takes 1.5 times longer, while eval takes more than 3 times as long as a simple invocation.

The Kernel#qualified_const_get alternative

Now, back to Kernel#qualified_const_get which was created by Gregory in this blog post a couple of years ago. It's looks a lot like const_get but is capable of figuring out nested classes too and it works just fine. However, it's very slow (it isn't native C code) and should probably be named something else because it has nothing to do with fetching constants any more. Kernel#fetch_class perhaps? But some numbers first:
puts "Ruby #{RUBY_VERSION}, #{RUBY_RELEASE_DATE}, #{RUBY_PLATFORM}"
require 'benchmark'
require 'qualified_const_get'

module Ooga
  class Booga
    def hello
      "hello!"
    end
  end
end

n = 1000000

Benchmark.bmbm(10) do |rpt|
rpt.report("simple invocation") do
  n.times {Ooga::Booga.new}
end

rpt.report("qualified const_get invocation") do
  n.times {Kernel.qualified_const_get('Ooga::Booga').new}
end

rpt.report("eval invocation") do
  n.times {eval('Ooga::Booga').new}
end
end
Output:
Ruby 1.8.6, 2007-06-07, i486-linux
Rehearsal -------------------------------------------------------
simple invocation     0.860000   0.120000   0.980000 (  0.991182)
qualified_const_get  16.620000   2.330000  18.950000 ( 19.052966)
eval invocation       4.500000   0.230000   4.730000 (  4.741436)
--------------------------------------------- total: 24.660000sec

                        user     system      total        real
simple invocation     0.990000   0.100000   1.090000 (  1.090303)
qualified_const_get  17.290000   2.420000  19.710000 ( 19.735733)
eval invocation       4.250000   0.170000   4.420000 (  4.429670)
See what I mean about qualified_const_get being slow because it isn't native?

Here's the implementation of Kernel#qualified_const_get quoted from his blog:
# http://redcorundum.blogspot.com/2006/05/kernelqualifiedconstget.html
module Kernel
  def qualified_const_get(str)
    path = str.to_s.split('::')
    from_root = path[0].empty?
    if from_root
      from_root = []
      path = path[1..-1]
    else
      start_ns = ((Class === self)||(Module === self)) ? self : self.class
      from_root = start_ns.to_s.split('::')
    end
    until from_root.empty?
      begin
        return (from_root+path).inject(Object) { |ns,name| ns.const_get(name) }
      rescue NameError
        from_root.delete_at(-1)
      end
    end
    path.inject(Object) { |ns,name| ns.const_get(name) }
  end
end


Summary
  • There are three choices when trying to convert a string to the corresponding class: Kernel#const_get, eval and Kernel#qualified_const_get
  • Kernel#const_get is the fastest, doesn't handle nested classes and works for all but the weirdest of scenarios (the class you're trying to get hold of isn't a named constant)
  • eval is significantly slower, but it works in any situation
  • Kernel#qualified_const_get is abysmally slow, but handles nested classes. However, until there is a native implementation, it loses to eval on every front


Looking for help with your Ruby/Rails project? Hire us!



If you liked this post, you could

subscribe to the feed

or 

Post a Comment