SimpleDelegator autoloading issue

I just blogged about an issue where SimpleDelegator in Rails breaks autoloading: SimpleDelegator autoloading issues with Rails – The Pug Automatic

Any thoughts about how (or if) Rails could solve this?

Interesting.

The problem is that the const_missing in Delegator issues ::Object.const_get by hand. Essentially this is the situation (modulus I still need coffee :):

  class Object     def self.const_missing(cname)       1     end   end

  class C < BasicObject     def self.const_missing(cname)       ::Object.const_get(cname)     end   end

  C::A # => 1

The problem there is that Delegator effectively throws the namespace by calling const_get on Object.

To have the constant autoloaded MyThing.const_missing should be invoked instead for Rails to know it should look into that namespace as one of possible candidates (it can because self.name returns "MyThing").

In general, to play by the rules const_missing in a class should call super if if is not able to find the constant, but Delegate cannot assume a lot since it inherits from BasicObject. On the other hand I don't quite see the point in that implementation, doing Object.const_get sounds quite arbitrary to me at first sight, doesn't make a lot of sense to me.

I don't have a workaround to propose right now, let me think about it, but that's more or less the situation.

Oh, good call with BasicObject. I see its docs (http://www.ruby-doc.org/core-2.1.1/BasicObject.html) mention this exact case:

Access to classes and modules from the Ruby standard library can be obtained in a BasicObject subclass by referencing the desired constant from the root like ::File or ::Enumerator. Like method_missing, const_missing can be used to delegate constant lookup to Object:

class MyObjectSystem < BasicObject
  def self.const_missing(name    )
::Object.const_get(name  )
end
end

I modified my blog post to implement a RailsySimpleDelegator that appears to handle both stdlib constant lookup and Rails autoloading: http://thepugautomatic.com/2014/03/simpledelegator-autoloading-issues-with-rails/

Maybe it would be the lesser of two evils to have Rails monkeypatch SimpleDelegator with that implementation of const_missing. The other evil being to let this gotcha remain.

I believe the culprit here is Delegator.

Delegator owns const_missing, it is all or nothing (reproduced from memory):

    class Delegator < BasicObject       def self.const_missing(cname)         ::Object.const_get(cname)       end     end

but it should not in my view: after trying its heuristic it should delegate to super if defined in case there are more heuristics above. Something like this (off the top of my head):

    class Delegator       def self.const_missing(cname)         if ::Object.const_defined?(cname)           ::Object.const_get(cname)         else           super if defined?(super)         end       end     end

The key question to address this today is: Which is the public API of AS::Dependencies? Can I include ModuleConstMissing in the singleton class of my class? Can I call load_missing_constant?

The answer is no, those belong to the implementation of AS::Dependencies. The public interface is pretty small, autoload_paths, require_dependency, and some other stuff, not much. Dependencies.rb is mostly about its contract.

So as of today, I'd say the best solution is the one based on require_depedency. That is using public interface, and looks clean in your source code compared to overriding const_missing. If the code was mine I would add a comment to explain the call.

Perhaps AS could provide a subclass of SimpleDelegator that does basically what I wrote above. But that's in a way duplicating (and thus depending) on the implementation of const_missing in Delegator today, is monkey patching in disguise in my opinion. Not really convinced.

This is a corner-case though. For this problem to happen we need a class that inherits from SimpleDelegator and in addition acts as a namespace. Not sure if it deserves reopening a class of stdlib to "fix it".

All taken into account, I believe this should be considered a gotcha of the combination SimpleDelegator + namespace + autoloading. And perhaps Delegator could be patched in future versions of Ruby.

I believe the culprit here is Delegator.

Delegator owns const_missing, it is all or nothing (reproduced from memory): but it should not in my view: after trying its heuristic it should delegate to super if defined in case there are more heuristics above. Something like this (off the top of my head):

That's a good point. I might attempt to contribute a patch to Ruby.

The key question to address this today is: Which is the public API of AS::Dependencies? Can I include ModuleConstMissing in the singleton class of my class? Can I call load_missing_constant? The answer is no, those belong to the implementation of AS::Dependencies. The public interface is pretty small, autoload_paths, require_dependency, and some other stuff, not much. Dependencies.rb is mostly about its contract. So as of today, I'd say the best solution is the one based on require_depedency. That is using public interface, and looks clean in your source code compared to overriding const_missing. If the code was mine I would add a comment to explain the call.

That also makes sense.

Perhaps AS could provide a subclass of SimpleDelegator that does basically what I wrote above. But that's in a way duplicating (and thus depending) on the implementation of const_missing in Delegator today, is monkey patching in disguise in my opinion. Not really convinced.

Yeah. I suppose if someone knows enough to look for that subclass, they could as well use require_dependency.

This is a corner-case though. For this problem to happen we need a class that inherits from SimpleDelegator and in addition acts as a namespace. Not sure if it deserves reopening a class of stdlib to "fix it".

You're probably right.

All taken into account, I believe this should be considered a gotcha of the combination SimpleDelegator + namespace + autoloading. And perhaps Delegator could be patched in future versions of Ruby.

Hopefully the blog post with keywords like SimpleDelegator, Rails, autoloading, "uninitialized constant" will be helpful if someone runs into this gotcha.

Thanks a lot for sharing your thoughts on this!

This small experiment illustrates that your solution does the trick:

If FakeDelegator's const_missing only does "::Object.const_get(name)", the last line of output will be "[ModuleConstMissing] Object missing NoSuch" instead of "[ModuleConstMissing] MyObject2 missing NoSuch".

Actually, it didn’t work within a real Rails app, so I did this for now: Stick this in the lib directory and use it instead of SimpleDelegator to solve this issue: http://thepugautomatic.com/2014/03/simpledelegator-autoloading-issues-with-rails/ · GitHub

If I find the time I’ll see if I can do something more like your solution. I definitely like the idea of modifying SimpleDelegator to be more general, not more tied to Rails.

Just a remark about this comment:

     "Load stdlib classes even though SimpleDelegator inherits from BasicObject."

The purpose of that const_get is to load top-level constants (which belong to Object), regardless of whether they come from stdlib. Classes that descend straight from BasicObject do not see top-level constants:

    X = 1

    class C < BasicObject       X # raises NameError     end

The reason for that error is that Object does not belong to the nesting, and it is not an ancestor of C, so the constant name resolution algorithm fails to find X. You need a fully-qualified ::X.

These kind of classes do not even see constants for core stuff:

    class D < BasicObject       String # raises NameError     end

String is no different than X in this regard, they are just top-level constants. String happens to be defined when your program boots, but other than that it has no special significance at all. It is an ordinary constant that stores a class object and Ruby finds it using the constant resolution algorithms at runtime, as any other constant.

I guess Delegator.const_missing wants somehow to hide from the user that it doesn't descend from Object.

Good point. Fixed.

The reason the "defined?(super)" solution doesn't work is, I realized, simply that the super call will call SimpleDelegator.const_missing instead of Rails' mixed-in Module.const_missing.

Can't think of a great way around that (but several nasty ways). Will keep thinking.

Yeah, that implementation based on super would be a replacement for the const_missing in Delegator, that would be the patch to send to ruby-core say.