Ruby: модуль, миксины и блоки запутывают?

Ниже приведен код, который я пытался запустить из Ruby Programming Book http://www.ruby-doc.org/docs/ProgrammingRuby/html/tut_modules.html

Почему метод product не дает правильного вывода? Я запустил его с помощью irb test.rb. И я бегу 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

Кстати: в Ruby 2.0 есть две функции, которые помогут вам справиться с вашими проблемами.

Module#prepend добавляет mixin к цепочке наследования, поэтому методы, определенные в методах переопределения mixin, определенных в модуле/классе, смешиваются.

Уточнения позволяют лексически охватить обезьянную палочку.

Здесь они находятся в действии (вы можете получить текущую сборку YARV 2.0 через RVM или ruby-build легко):

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

Поскольку этот пример кода был написан, Array получил метод #product, и вы видите вывод этого конкретного метода, Переименуйте свой модульный метод на что-то вроде product_new.

Ответ 3

Добавьте эту строку в конец вашего кода:

p Array.ancestors

и вы получите (в Ruby 1.9.3):

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

Array является подклассом Object и имеет указатель на суперкласс для Object. Поскольку Enumerable смешивается (включается) Array, указатель суперкласса Array указывает на Enumerable, а оттуда - на Object. Когда вы включаете Inject, указатель суперкласса Array указывает на Inject, а оттуда на Enumerable. Когда вы пишете

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

механизм поиска метода начинается с объекта экземпляра [1, 2, 3, 4, 5], переходит в его класс Array и находит продукт (новый в 1.9). Если вы запустили тот же код в Ruby 1.8, механизм поиска метода начинается с объекта экземпляра [1, 2, 3, 4, 5], переходит в его класс Array, не находит продукт, не поднимается над цепочкой суперкласса и не находит продукт в Inject, и вы получите результат 120, как ожидалось.

Вы найдете хорошее объяснение модулей и микшинов с графическими изображениями в Pickaxe http://pragprog.com/book/ruby3/programming-ruby-1-9

Я знал, что видел, что некоторые из них просят использовать метод prepend для включения модуля до, между экземпляром и его классом, чтобы механизм поиска обнаруживал включенные методы перед классами. Я сделал soach в SO с "[ruby] prepend module вместо include" и нашел среди прочего следующее:

Почему включение этого модуля не отменяет динамически сгенерированный метод?

Ответ 4

В ответ на @zeronone "Как избежать таких конфликтов пространства имен?"

Избегайте, если это возможно, первое правило для основных классов ядра. Лучший способ сделать это (IMO) - это подкласс 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 может быть языком OO, но поскольку он настолько динамичен, иногда (я нахожу), подклассификация забывается как полезный способ сделать что-то, и, следовательно, существует большая зависимость от базовых структур данных массивов Array, Hash и String, что приводит к слишком большому повторному открытию этих классов.

Ответ 5

Следующий код не очень проработан. Просто чтобы показать вам, что сегодня у вас уже есть средства, такие как крючки, вызываемые Ruby при возникновении определенных событий, для проверки того, какой метод (из включенного класса или включенного модуля) будет использоваться/не использоваться.

module Inject
    def self.append_features(p_host) # don't use included, it 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

Отдых как в вашем посте. Исполнение:

$ 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