Ruby: Module, Mixins and Blocks confusing?

2019-02-19 12:02发布

问题:

Following is the code I tried to run from the Ruby Programming Book http://www.ruby-doc.org/docs/ProgrammingRuby/html/tut_modules.html

Why doesn't the product method give the right output? I ran it with irb test.rb. And I am running Ruby 1.9.3p194.

module Inject
  def inject(n)
    each do |value|
      n = yield(n, value)
    end
    n
  end

  def sum(initial = 0)
    inject(initial) { |n, value| n + value }
  end

  def product(initial = 1)
    inject(initial) { |n, value| n * value }
  end
end

class Array
  include Inject
end

[1, 2, 3, 4, 5].sum            ## 15
[1, 2, 3, 4, 5].product        ## [[1], [2], [3], [4], [5]]

回答1:

By the way: in Ruby 2.0, there are two features which help you with both your problems.

Module#prepend prepends a mixin to the inheritance chain, so that methods defined in the mixin override methods defined in the module/class it is being mixed into.

Refinements allow lexically scoped monkeypatching.

Here they are in action (you can get a current build of YARV 2.0 via RVM or ruby-build easily):

module Sum
  def sum(initial=0)
    inject(initial, :+)
  end
end

module ArrayWithSum
  refine Array do
    prepend Sum
  end
end

class Foo
  using ArrayWithSum

  p [1, 2, 3].sum
  # 6
end

p [1, 2, 3].sum
# NoMethodError: undefined method `sum' for [1, 2, 3]:Array

using ArrayWithSum
p [1, 2, 3].sum
# 6


回答2:

Since that code example was written, Array has gained a #product method and you're seeing the output of that particular method. Rename your module's method to something like product_new.



回答3:

Add this line at the end of your code :

p Array.ancestors

and you get (in Ruby 1.9.3) :

[Array, Inject, Enumerable, Object, Kernel, BasicObject]

Array is a subclass of Object and has a superclass pointer to Object. As Enumerable is mixed in (included) by Array, the superclass pointer of Array points to Enumerable, and from there to Object. When you include Inject, the superclass pointer of Array points to Inject, and from there to Enumerable. When you write

[1, 2, 3, 4, 5].product

the method search mechanism starts at the instance object [1, 2, 3, 4, 5], goes to its class Array, and finds product (new in 1.9) there. If you run the same code in Ruby 1.8, the method search mechanism starts at the instance object [1, 2, 3, 4, 5], goes to its class Array, does not find product, goes up the superclass chain, and finds product in Inject, and you get the result 120 as expected.

You find a good explanation of Modules and Mixins with graphic pictures in the Pickaxe http://pragprog.com/book/ruby3/programming-ruby-1-9

I knew I had seen that some are asking for a prepend method to include a module before, between the instance and its class, so that the search mechanism finds included methods before the ones of the class. I made a seach in SO with "[ruby]prepend module instead of include" and found among others this :

Why does including this module not override a dynamically-generated method?



回答4:

In response to @zeronone "How can we avoid such namespace clashes?"

Avoid monkeypatching core classes wherever possible is the first rule. A better way to do this (IMO) would be to subclass Array:

class MyArray < Array
  include Inject 
  # or you could just dispense with the module and define this directly.
end


xs = MyArray.new([1, 2, 3, 4, 5])
# => [1, 2, 3, 4, 5]
xs.sum
# => 15
xs.product
# => 120
[1, 2, 3, 4, 5].product
# => [[1], [2], [3], [4], [5]]

Ruby may be an OO language, but because it is so dynamic sometimes (I find) subclassing gets forgotten as a useful way to do things, and hence there is an over reliance on the basic data structures of Array, Hash and String, which then leads to far too much re-opening of these classes.



回答5:

The following code is not very elaborated. Just to show you that today you already have means, like the hooks called by Ruby when certain events occur, to check which method (from the including class or the included module) will be used/not used.

module Inject
    def self.append_features(p_host) # don't use included, it's too late
        puts "#{self} included into #{p_host}"
        methods_of_this_module = self.instance_methods(false).sort
        print "methods of #{self} : "; p methods_of_this_module
        first_letter = []
        methods_of_this_module.each do |m|
            first_letter << m[0, 2]
        end
        print 'selection to reduce the display : '; p first_letter
        methods_of_host_class = p_host.instance_methods(true).sort
        subset = methods_of_host_class.select { |m| m if first_letter.include?(m[0, 2]) }
        print "methods of #{p_host} we are interested in: "; p subset
        methods_of_this_module.each do |m|
            puts "#{self.name}##{m} will not be used" if methods_of_host_class.include? m
        end

        super # <-- don't forget it !
    end

Rest as in your post. Execution :

$ ruby -v
ruby 1.8.6 (2010-09-02 patchlevel 420) [i686-darwin12.2.0]
$ ruby -w tinject.rb 
Inject included into Array
methods of Inject : ["inject", "product", "sum"]
selection to reduce the display : ["in", "pr", "su"]
methods of Array we are interested in: ["include?", "index",  
 ..., "inject", "insert", ..., "instance_variables", "private_methods", "protected_methods"]
Inject#inject will not be used
$ rvm use 1.9.2
...
$ ruby -v
ruby 1.9.2p320 (2012-04-20 revision 35421) [x86_64-darwin12.2.0]
$ ruby -w tinject.rb 
Inject included into Array
methods of Inject : [:inject, :product, :sum]
selection to reduce the display : ["in", "pr", "su"]
methods of Array we are interested in: [:include?, :index, ..., :inject, :insert, 
..., :private_methods, :product, :protected_methods]
Inject#inject will not be used
Inject#product will not be used