21

I love the autoload functionality of Ruby; however, it's going away in future versions of Ruby since it was never thread-safe.

So right now I would like to pretend it's already gone and write my code without it, by implementing the lazy-loading mechanism myself. I'd like to implement it in the simplest way possible (I don't care about thread-safety right now). Ruby should allow us to do this.

Let's start by augmenting a class' const_missing:

class Dummy
  def self.const_missing(const)
    puts "const_missing(#{const.inspect})"
    super(const)
  end
end

Ruby will call this special method when we try to reference a constant under "Dummy" that's missing, for instance if we try to reference "Dummy::Hello", it will call const_missing with the Symbol :Hello. This is exactly what we need, so let's take it further:

class Dummy
  def self.const_missing(const)
    if :OAuth == const
      require 'dummy/oauth'
      const_get(const)      # warning: possible endless loop!
    else
      super(const)
    end
  end
end

Now if we reference "Dummy::OAuth", it will require the "dummy/oauth.rb" file which is expected to define the "Dummy::OAuth" constant. There's a possibility of an endless loop when we call const_get (since it can call const_missing internally), but guarding against that is outside the scope of this question.

The big problem is, this whole solution breaks down if there exists a module named "OAuth" in the top-level namespace. Referencing "Dummy::OAuth" will skip its const_missing and just return the "OAuth" from the top-level. Most Ruby implementations will also make a warning about this:

warning: toplevel constant OAuth referenced by Dummy::OAuth

This was reported as a problem way back in 2003 but I couldn't find evidence that the Ruby core team was ever concerned about this. Today, most popular Ruby implementations carry the same behavior.

The problem is that const_missing is silently skipped in favor of a constant in the top-level namespace. This wouldn't happen if "Dummy::OAuth" was declared with Ruby's autoload functionality. Any ideas how to work around this?

mislav
  • 14,919
  • 8
  • 47
  • 63
  • This seems like a silly suggestion, but can you look at the C source of `autoload`? I'm sure you can find it somewhere in the Ruby source. If you can't do it in straight Ruby, there is the option of creating a C extension (which have access to the underbelly of the interpreter). – Linuxios Jan 29 '12 at 14:48
  • sounds like a brute force thing, but couldn't you `remove_const` on the top-level class? – phoet Jan 29 '12 at 15:34
  • @phoet: it's dangerous and brittle. Besides, I have no hook at which I can perform such a hack. – mislav Jan 29 '12 at 18:05

3 Answers3

5

This was raised in a Rails ticket some time ago and when I investigated it there appeared to be no way round it. The problem is that Ruby will search the ancestors before calling const_missing and since all classes have Object as an ancestor then any top-level constants will always be found. If you can restrict yourself to only using modules for namespacing then it will work since they do not have Object as an ancestor, e.g:

>> class A; end
>> class B; end
>> B::A
(irb):3: warning: toplevel constant A referenced by B::A

>> B.ancestors
=> [B, Object, Kernel, BasicObject]

>> module C; end
>> module D; end
>> D::C
NameError: uninitialized constant D::C

>> D.ancestors
=> [D]
pixeltrix
  • 991
  • 7
  • 7
  • Yeah it's unfortunate that we can't get it to work with classes, however it's good to know that at least this works with modules. Thanks! – mislav Jan 29 '12 at 21:31
0

Lazy loading is a very common design pattern, you can implementing it in many ways. like :

class Object
  def bind(key, &block)
    @hooks ||= Hash.new{|h,k|h[k]=[]}
    @hooks[key.to_sym] << [self,block]
  end

  def trigger(key)
    @hooks[key.to_sym].each { |context,block| block.call(context) }
  end
end

Then you can

 bind :json do
   require 'json'
 end

 begin
   JSON.parse("[1,2]")
 rescue
  trigger :json
  retry
 end
jiahut
  • 1,451
  • 15
  • 14
0

I get your problem on ree 1.8.7 (you don't mention a specific version) if I use const_get inside const_missing, but not if I use ::. I don't love using eval, but it does work here:

class Dummy
  def self.const_missing(const)
    if :OAuth == const
      require 'dummy/oauth'
      eval "self::#{const}"
    else
      super(const)
    end
  end
end

module Hello
end

Dummy.const_get :Hello # => ::Hello
Dummy::Hello           # => Dummy::Hello

I wish Module had a :: method so you could do self.send :"::", const.

James A. Rosen
  • 64,193
  • 61
  • 179
  • 261
  • Sorry for not mentioning specific versions. My problem is present across 1.8.7, 1.9.2, 1.9.3, Rubinius (both stable and edge) and other. I will try your suggestion, but from what I got in my testing the `const_missing` method wasn't called at all. So it didn't matter how I implement it. – mislav Jan 29 '12 at 18:03
  • That depends on whether you use `Dummy.const_get :Hello` or `Dummy::Hello` *outside* the `const_missing` method. Only `::Hello` will trigger the `const_missing` -- or at least that's true on the one Ruby implementation I tried. – James A. Rosen Jan 29 '12 at 18:32