SimpleDelegator autoloading issue

506 views
Skip to first unread message

Henrik N

unread,
Mar 2, 2014, 3:23:56 AM3/2/14
to rubyonra...@googlegroups.com
I just blogged about an issue where SimpleDelegator in Rails breaks autoloading: http://thepugautomatic.com/2014/03/simpledelegator-autoloading-issues-with-rails/

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

Xavier Noria

unread,
Mar 2, 2014, 4:15:07 AM3/2/14
to rubyonrails-core
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.

Henrik Nyh

unread,
Mar 2, 2014, 12:14:11 PM3/2/14
to rubyonra...@googlegroups.com
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


--
You received this message because you are subscribed to a topic in the Google Groups "Ruby on Rails: Core" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/rubyonrails-core/PjGUK72BmFA/unsubscribe.
To unsubscribe from this group and all its topics, send an email to rubyonrails-co...@googlegroups.com.
To post to this group, send email to rubyonra...@googlegroups.com.
Visit this group at http://groups.google.com/group/rubyonrails-core.
For more options, visit https://groups.google.com/groups/opt_out.

Henrik Nyh

unread,
Mar 2, 2014, 12:31:05 PM3/2/14
to rubyonra...@googlegroups.com
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.

Xavier Noria

unread,
Mar 2, 2014, 2:11:26 PM3/2/14
to rubyonrails-core
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.

Henrik Nyh

unread,
Mar 2, 2014, 4:20:17 PM3/2/14
to rubyonra...@googlegroups.com
On Sun, Mar 2, 2014 at 8:11 PM, Xavier Noria <f...@hashref.com> wrote:
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!

Henrik Nyh

unread,
Mar 2, 2014, 6:04:14 PM3/2/14
to rubyonra...@googlegroups.com
On Sun, Mar 2, 2014 at 10:20 PM, Henrik Nyh <hen...@nyh.se> wrote:
On Sun, Mar 2, 2014 at 8:11 PM, Xavier Noria <f...@hashref.com> wrote:
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):

This small experiment illustrates that your solution does the trick: https://gist.github.com/henrik/9314943

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".

Henrik N

unread,
Mar 3, 2014, 5:06:06 AM3/3/14
to rubyonra...@googlegroups.com
On Monday, March 3, 2014 12:04:14 AM UTC+1, Henrik N wrote:
On Sun, Mar 2, 2014 at 10:20 PM, Henrik Nyh <hen...@nyh.se> wrote:
On Sun, Mar 2, 2014 at 8:11 PM, Xavier Noria <f...@hashref.com> wrote:
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):

This small experiment illustrates that your solution does the trick: https://gist.github.com/henrik/9314943

Actually, it didn't work within a real Rails app, so I did this for now: https://gist.github.com/henrik/9321626

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.

Xavier Noria

unread,
Mar 3, 2014, 6:57:09 AM3/3/14
to rubyonrails-core
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.

Henrik Nyh

unread,
Mar 3, 2014, 1:03:37 PM3/3/14
to rubyonra...@googlegroups.com
On Mon, Mar 3, 2014 at 12:57 PM, Xavier Noria <f...@hashref.com> wrote:
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.

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.

Xavier Noria

unread,
Mar 3, 2014, 2:51:20 PM3/3/14
to rubyonrails-core
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.
Reply all
Reply to author
Forward
0 new messages