Вызовите юристов; это контракт.

Я хотел бы продемонстрировать на высоком уровне, как Enumerable модуль Ruby требует #each метода от классов, которые его включают.

Я слышал, как Армандо Фокс, профессор EECS из Калифорнийского университета в Беркли, описал Enumerable как «контракт» между классом, который включает его, и самим модулем. То есть Enumerable выполняет следующие действия с классами, которые его включают:

«Если вы предоставите мне способ итерации или перечисления для каждого собственного экземпляра, я дам вам взамен различные полезные методы».

Эту итерацию для Enumerable классы обеспечивают с помощью метода #each. Это контракт, который обменивает #each на использование методов из Enumerable. Сюда входят map, select и inject.

Из The Ruby Docs:

Класс должен предоставить метод #each, который выдает последовательные члены коллекции.

Имеет смысл, что для того, чтобы класс мог использовать Enumerable,, класс должен предоставлять #each метод; в противном случае Enumerable не знал бы, как последовательно получить доступ к каждому элементу в коллекции.

Погружение в контракт

Давайте посмотрим, что происходит, когда класс не предоставляет #each метод, а экземпляр класса пытается вызвать select и map. Для этого мы создадим FakeArrayWrapper класс, в котором отсутствует метод #each. Мы можем рассматривать FakeArrayWrapper как оболочку для класса Array.

class FakeArrayWrapper
  include Enumerable
  def initialize(*args)
    @fake_array = args.flatten
  end
    
  #we omit the #each method here
end

Когда мы пытаемся вызвать методEnumerable, select, в экземпляреFakeArrayWrapper, вызов возвращает ошибку.

fake_array_instance = FakeArrayWrapper.new([1,2,3,4])
fake_array_instance.select {|n| n == 1} 
# =>
fake_array.rb:17:in `select': undefined method `each' for #<FakeArrayWrapper:0x007fed5e816c00 @fake_array=[1, 2, 3, 4]> (NoMethodError)
 from fake_array.rb:17:in `<main>'

Аналогично получаем ошибку при вызове map:

fake_array_instance = FakeArrayWrapper.new([1,2,3,4])
fake_array_instance.map {|n| n + 1}
# =>
fake_array.rb:20:in `map': undefined method `each' for #<FakeArrayWrapper:0x007f87908179e0 @fake_array=[1, 2, 3, 4]> (NoMethodError)
 from fake_array.rb:20:in `<main>'

Эти NoMethodError ошибки говорят нам, что select и map из Enumerable пытались использовать метод с именем #each для вызывающего объекта (в данном случае fake_array_instance), но этот метод не существует для экземпляров FakeArrayWrapper.

Мы это уже знали; мы не определяли #each в классе FakeArrayWrapper.

Руби говорила здесь:

«Я пытался позвонить select и map на вашем fake_array_instance, но для этого мне нужно было, чтобы вы дали мне #each метод для fake_array_instance».

Обратите внимание, что класс FakeArrayWrapper не меняет поведения реального класса Array. Вызов тех же Enumerable методов на реальном Array экземпляре по-прежнему возвращает ожидаемый:

real_array = [1,2,3,4]
real_array.select {|n| n == 1} 
# =>[1]
real_array.map {|n| n + 1} 
# =>[2,3,4,5]

Теперь, если мы реализуем метод #each в FakeArrayWrapper:

class FakeArrayWrapper
  include Enumerable
  def initialize(*args)
    @fake_array = args.flatten
  end
  #now we implement #each
  def each(&block)
    @fake_array.each(&block) 
    self #return the original array
  end
end

методы Enumerable возвращают ожидаемые значения для fake_array_instance:

fake_array_instance = FakeArrayWrapper.new([1,2,3,4])
fake_array_instance.select {|n| n == 1} 
# =>[1]
fake_array_instance.map {|n| n + 1} 
# =>[2,3,4,5]

Мы больше не получаем NoMethodError, потому что теперь реализовано FakeArrayWrapper#each, и select и map могут без проблем вызывать fake_array_instance.each(&block).

Итак, чтобы классы, включающие Enumerable, могли использовать методы Enumerable, включающий класс должен предоставлять #each метод, позволяющий Enumerable выполнять итерацию по элементам экземпляра класса.

В другом посте я реализую свою собственную версию Enumerable, называемую MyEnumerable, с map и select, чтобы пролить свет на внутреннюю работу Enumerable.