Search code examples
rubyprepend

Is there a reason why Ruby's prepend behaves differently when used with modules versus classes?


While monkey-patching a module from a Rails engine, we found that if you prepend a module B to another module A, the prepended module B won't be added to the ancestors of classes that have already included module A prior to the prepend. To illustrate:

module A
  def a
    puts 'a'
  end
end

class B
  include A
end

module C
  def a
    puts 'c'
  end
  A.prepend self
end

class D
  include A
end

B.new.a # prints: a
D.new.a # prints: c

B.ancestors #=> [B, A, Object, Kernel, BasicObject]
D.ancestors #=> [D, C, A, Object, Kernel, BasicObject]

Both classes B & D include A, but only D takes the new behavior as it's the one defined after we call prepend.

In contrast, we learned that class inheritance does not exhibit this behavior:

class A
  def a
    puts 'a'
  end
end

class B < A
end

module C
  def a
    puts 'c'
  end

  A.prepend self
end

class D < A
end

B.new.a # prints: c
D.new.a # prints: c

B.ancestors #=> [B, C, A, Object, Kernel, BasicObject]
D.ancestors #=> [D, C, A, Object, Kernel, BasicObject]

This time, both classes B & D have taken the new behavior, even if B was defined before we called prepend.

Is there a reason why prepend behaves differently when used with modules versus classes? I'm assuming this is by design, but it does present a gotcha when using prepend with modules.


Solution

  • https://bugs.ruby-lang.org/issues/9573 shows a similar behavior concerning classes and modules. The bug report was posted on 2014 and was only closed 2020. Based on that report, I've manually confirmed that

    • This behavior still appears in ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-linux], but
    • This behavior no longer appears in ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-linux]