Вызовите юристов; это контракт.
Я хотел бы продемонстрировать на высоком уровне, как 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
.