Arion
Arion

Reputation: 1880

Redefining class in Ruby seems to include wrong module

I come from a Python background, and I was trying to understand how Ruby includes compare to Python's multiple inheritance. Specifically, I was setting up two modules with the same method to see how super work.

I included Mod, then Mod2, then Mod again. However, A still seems to be referencing Mod2.

module Mod
  def foo(x)
    x**2
  end
end
module Mod2
  def foo(x)
    x*2
  end
end

class A
  include Mod
  def foo(x)
    super(x) + 1
  end
end
A.new.foo(5) == 26 # true

class A
  include Mod2
  def foo(x)
    super(x) + 1
  end
end
A.new.foo(5) == 11 # true

class A
  include Mod
  def foo(x)
    super(x) + 1
  end
end
A.new.foo(5) == 26 # false. Is 11

Why isn't the third A.new.foo(5) return 26?

Upvotes: 0

Views: 354

Answers (3)

m. simon borg
m. simon borg

Reputation: 2575

Ruby finds super by searching the class/module's ancestry chain one class/module at a time until it finds the definition of the method it's looking for. Sort of like a bus with many stops, looking for the right place to get off, but it always gets off at the first opportunity. If it gets all the way up to BasicObject and doesn't find it's stop it will raise a NoMethodError.

When you include a module, you're adding it to the ancestry chain just above the including class/module. You can find the ancestors, in order, by calling Module.ancestors.You can include the same module many times, but it will only be added to the ancestry chain once. Consider this:

module Mod
  def foo(x)
    x**2
  end
end

module Mod2
  def foo(x)
    x*2
  end
end

class A
  include Mod
  def foo(x)
    super(x) + 1
  end
end

A.ancestors # => [A, Mod, Object, Kernel, BasicObject]

class A
  include Mod2
  def foo(x)
    super(x) + 1
  end
end

A.ancestors # => [A, Mod2, Mod, Object, Kernel, BasicObject]

class A
  include Mod
  def foo(x)
    super(x) + 1
  end
end

A.ancestors # => [A, Mod2, Mod, Object, Kernel, BasicObject]

Mod was already in the ancestry chain the second time it was included, so nothing new happened. It stays put, and super still hits Mod2 first. In short, you can't "override" newer modules by including older modules that were already there. There are other things you can do though. You could define a Mod3, or maybe ModOverride, with the same definition of foo as was in Mod and include that, or look into refinements Ruby 2.0 How do I uninclude a module out from a module after including it?

Side note

As other answers noted, you don't need to redefine A#foo every time if the definition is staying the same.

Upvotes: 0

Josh Warfield
Josh Warfield

Reputation: 38

I think what you may be missing is that the second class A ... end is not redefining the class, it's adding to it. You’re allowed to "open" a class multiple times in ruby and (re-)define whatever you want. That, and what you seem to have already figured out, which is that include more or less just drops the code from the module into that spot in the class definition, unless that module has already been included in which case it does nothing.

You can do this with any class at all (whether advisable or not)

class Hash
  def to_a
    'lol you probably wanted an array here'
  end
end
{foo: :bar}.to_a # Returns above string instead of [:foo, :bar]

Upvotes: 1

Arion
Arion

Reputation: 1880

Defining class A(object) in PythonCreates an object with a type of type and __name__ of A, then assigns that object to the label A. A second class A(object) definition creates a whole new type object that is then put into label A.

Classes in ruby aren't stored in labels, and they can be opened to add more functionality. A good example of this is 2.days. Rails defines days on integer. Without Rails, 2.days fails.

The 3 class definitions above are roughly equivalent to:

class A
  include Mod
  def foo(x)
    super(x) + 1
  end

  include Mod2
  def foo(x)
    super(x) + 1
  end

  include Mod
  def foo(x)
    super(x) + 1
  end
end

When ruby sees the second include Mod, it knows Mod has already been included on the class and skips it, leaving foo from Mod2 as the most recent definition.

If you remove foo from later definitions, A still has access to it:

class A
  include Mod
  def foo(x)
    super(x) + 1
  end
end
A.new.foo(5) == 26 # true

class A
  include Mod2
end
A.new.foo(5) == 11 # true

class A
  include Mod
end
A.new.foo(5) == 11

Upvotes: 1

Related Questions