routes.url_helpers doesn't include properly

297 views
Skip to first unread message

Nick Sutterer

unread,
Jun 27, 2012, 12:43:00 PM6/27/12
to rubyonra...@googlegroups.com
Hi friends,

I found a bug (?) with routes.url_helpers. Let's say my routes file contains

  resources :fruits

Now I have the following setup:

module A
end

module B
  include A
end

A.module_eval do
   include Rails.application.routes.url_helpers
end

Object.new.extend(A).fruit_path #=> That works.
Object.new.extend(B).fruit_path #=> NoMethodError: undefined method `fruit_path' for #<Object:0x98e56d0>

In a proper Ruby setup B should have references to any method in A. Obviously, this doesn't work with url_helpers. I didn't check its implementation so far, is there any magic done inside the url_helpers that breaks the expected behaviour?

Nick

Nick Sutterer

unread,
Jun 28, 2012, 3:41:13 AM6/28/12
to rubyonra...@googlegroups.com
I'd like to state that this behaviour usually works in Ruby:


module A
end

module B
  include A
end

A.module_eval do
  def a
    "Hey from A!"
  end
end

Object.new.extend(B).a #=> "Hey from A!"

Nick Sutterer

unread,
Jun 29, 2012, 5:08:13 AM6/29/12
to rubyonra...@googlegroups.com
I just got educated by drogus that the problem is the module_eval with include:


module UrlHelpers
  def fruit_path
  end
end

A.module_eval do
  include UrlHelpers
end

Object.new.extend(B).fruit_path
#=> NoMethodError: undefined method `fruit_path' for #<Object:0x964c540>

So obviously, when including modules into source modules it is not propagated properly to the includer.

Xavier Noria

unread,
Jun 29, 2012, 8:35:52 AM6/29/12
to rubyonra...@googlegroups.com
On Fri, Jun 29, 2012 at 11:08 AM, Nick Sutterer <apot...@gmail.com> wrote:
I just got educated by drogus that the problem is the module_eval with include:

This is a gotcha in Ruby.

As you probably know, the interpreter keeps a linear representation of the ancestors of a base class or module. Algorithms that walk up the ancestors go over that linearization following pointers up, as in a linked list, rather than following pointers recursively which would be the immediate mental model for this.

OK, if you add methods or constants to any class or module in that ancestry chain they are seen:

    module M
    end

    class C
      include M
    end

    module M
      def foo
      end
    end

    C.new.foo # WORKS

If you include more modules into the base class or module, their methods are found:

    class C
    end

    module M
      def foo
      end
    end

    class C
      include M
    end

    C.new.foo # WORKS

The linear ancestry that exists behind the scenes gets updated to reflect the new ancestor.

But if once the linearization is done you modify the 2nd-degree ancestors, those are not propagated:

    module M
    end

    class C
      include M
    end

    module N
      def bar
      end
    end

    module M
      include N
    end

    C.new.bar # DOES NOT WORK

As you see the linearization of the ancestry chain of C does not get updated with N, albeit if you computed the ancestry chain in that very moment N should belong to it.

I asked for the rationale behind this, conceptually does not seem coherent. Matz said it could be changed if someone came with an implementation that was performant enough[*].

Xavier

Reply all
Reply to author
Forward
0 new messages