SimpleDelegator autoloading issue

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?

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

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.

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.