Какова цель класса Enumerator в Ruby

Если я создам Enumertor следующим образом:

enum = [1,2,3].each => #<Enumerator: [1, 2, 3]:each> 

enum — это перечислитель. Какова цель этого объекта? Я не могу сказать это:

enum { |i| puts i }

Но я могу сказать следующее:

enum.each { |i| puts i }

Это кажется излишним, потому что Enumerator был создан с помощью .each. Похоже, он хранит некоторые данные о методе each.

Я не понимаю, что здесь происходит. Я уверен, что есть какая-то логическая причина, по которой у нас есть этот класс Enumerator, но что он может делать такого, чего не может массив? Я думал, может быть, это предок Array и других Enumerables, но, похоже, это не так. В чем именно причина существования класса Enumerator и в каком контексте он когда-либо будет использоваться?


person ordinary    schedule 06.06.2013    source источник
comment
В этой статье показаны некоторые примеры использования Enumerators для отложенных вычислений.   -  person Darshan Rivka Whittle    schedule 07.06.2013


Ответы (5)


Что произойдет, если вы сделаете enum = [1,2,3].each; enum.next?:

enum = [1,2,3].each
=> #<Enumerator: [1, 2, 3]:each>
enum.next
=> 1
enum.next
=> 2
enum.next
=> 3
enum.next
StopIteration: iteration reached an end

Это может быть полезно, если у вас есть перечислитель, выполняющий вычисления, например калькулятор простых чисел или генератор последовательности Фибоначчи. Это обеспечивает гибкость в том, как вы пишете свой код.

person the Tin Man    schedule 06.06.2013

Я думаю, основная цель — получать элементы по запросу, а не получать их все в одном цикле. Я имею в виду что-то вроде этого:

e = [1, 2, 3].each
... do stuff ...
first = e.next
... do stuff with first ...
second = e.next
... do more stuff with second ...

Обратите внимание, что эти части do stuff могут находиться в разных функциях далеко друг от друга.

Лениво оцениваемые бесконечные последовательности (например, простые числа, числа Фибоначчи, строковые ключи, такие как 'a'..'z','aa'..'az','ba'..'zz','aaa'.. и т. д.), являются хорошим примером использования счетчиков.

person Sergey Bolgov    schedule 06.06.2013

Как уже было сказано, Enumerator пригодится, когда вы хотите перебрать последовательность данных потенциально бесконечной длины.

Возьмем, к примеру, генератор простых чисел prime_generator, который расширяет Enumerator. Если мы хотим получить первые 5 простых чисел, мы можем просто написать prime_generator.take 5 вместо того, чтобы встраивать «предел» в логику генерации. Таким образом, мы можем разделить генерацию простых чисел и извлечение определенной суммы из сгенерированных простых чисел, что делает генератор многоразовым.

Мне, например, нравится цепочка методов с использованием методов Enumerable, возвращающих Enumerator, как в следующем примере (это может быть не «цель», но я хочу просто указать на его эстетический аспект):

prime_generator.take_while{|p| p < n}.each_cons(2).find_all{|pair| pair[1] - pair[0] == 2}

Здесь prime_generator — это экземпляр Enumerator, который возвращает простые числа одно за другим. Мы можем взять простые числа меньше n, используя метод take_while Enumerable. Оба метода each_cons и find_all возвращают Enumerator, поэтому их можно объединить в цепочку. Этот пример предназначен для генерации простых чисел-близнецов ниже n. Это может быть неэффективной реализацией, но ее легко написать в строке и ИМХО, подходящей для прототипирования.

Вот довольно простая реализация prime_generator на основе Enumerator:

def prime?(n)
  n == 2 or
    (n >= 3 and n.odd? and (3...n).step(2).all?{|k| n%k != 0})
end
prime_generator = Enumerator.new do |yielder|
  n = 1
  while true
    yielder << n if prime? n
    n += 1
  end
end
person M. Shiina    schedule 07.06.2013

Перечислители можно комбинировать:

array.each.with_index { |el, idx| ... }
person samuil    schedule 07.06.2013

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

В Ruby класс Enumerator позволяет использовать внешние итераторы. И как только вы поймете, что такое внешние итераторы, вы начнете открывать множество преимуществ. Во-первых, давайте посмотрим, как класс Enumerator упрощает внешнюю итерацию:

class Fruit
  def initialize
    @kinds = %w(apple orange pear banana)
  end

  def kinds
    yield @kinds.shift
    yield @kinds.shift
    yield @kinds.shift
    yield @kinds.shift
  end
end

f = Fruit.new
enum = f.to_enum(:kinds)
enum.next
 => "apple" 
f.instance_variable_get :@kinds
 => ["orange", "pear", "banana"] 
enum.next
 => "orange" 
 f.instance_variable_get :@kinds
 => ["pear", "banana"] 
enum.next
 => "pear" 
f.instance_variable_get :@kinds
 => ["banana"] 
 enum.next
 => "banana"
f.instance_variable_get :@kinds
 => [] 
 enum.next
StopIteration: iteration reached an end

Важно отметить, что вызов to_enum для объекта и передача символа, соответствующего методу, создаст экземпляр класса Enumerator, и в нашем примере локальная переменная enum содержит экземпляр Enumerator. Затем мы используем внешнюю итерацию для обхода созданного нами метода перечисления. Наш метод перечисления называется «виды», и обратите внимание, что мы используем метод yield, который мы обычно делаем с блоками. Здесь перечислитель будет выдавать одно значение за раз. Он делает паузу после каждого выхода. При запросе другого значения он возобновится сразу после последнего полученного значения и будет выполняться до следующего полученного значения. Когда ничего не осталось, и вы вызываете next, он вызовет исключение StopIteration.

Так в чем сила внешней итерации в Ruby? Есть несколько преимуществ, и я выделю некоторые из них. Во-первых, класс Enumerator допускает цепочку. Например, with_index определен в классе Enumerator и позволяет нам указать начальное значение для итерации при переборе объекта Enumerator:

f.instance_variable_set :@kinds, %w(apple orange pear banana)
enum.rewind
enum.with_index(1) do |name, i| 
  puts "#{name}: #{i}"
end

apple: 1
orange: 2
pear: 3
banana: 4

Во-вторых, он предоставляет ТОННУ полезных удобных методов из модуля Enumerable. Помните, что Enumerator — это класс, а Enumerable — это модуль, но модуль Enumerable включен в класс Enumerator, поэтому перечислители являются Enumerable:

Enumerator.ancestors
 => [Enumerator, Enumerable, Object, Kernel, BasicObject] 
 f.instance_variable_set :@kinds, %w(apple orange pear banana)
 enum.rewind
 enum.detect {|kind| kind =~ /^a/}
 => "apple" 
 enum
 => #<Enumerator: #<Fruit:0x007fb86c09bdf8 @kinds=["orange", "pear", "banana"]>:kinds>

И есть еще одно важное преимущество Enumerator, которое может быть не сразу понятно. Позвольте мне объяснить это с помощью демонстрации. Как вы, наверное, знаете, любой из ваших пользовательских классов можно сделать Enumerable, включив модуль Enumerable и определив метод каждого экземпляра:

class Fruit
  include Enumerable

  attr_accessor :kinds

  def initialize
    @kinds = %w(apple orange pear banana)
  end

  def each
    @kinds.each { |kind| yield kind }
  end
end

Это здорово. Теперь у нас есть масса полезных методов экземпляра Enumerable, таких как chunk, drop_while, flat_map, grep, lazy, partition, reduce, take_while и другие.

f.partition {|kind| kind =~ /^a/ }
 => [["apple"], ["orange", "pear", "banana"]] 

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

module Enumerable
  def reduce(acc)
    each do |value|
      acc = yield(acc, value)
    end
    acc
  end
end

Обратите внимание, как он передает блок методу each, поэтому ожидается, что наш метод each вернет что-то обратно в блок.

Но посмотрите, что произойдет, если клиентский код вызовет метод each без указания блока:

f.each
LocalJumpError: no block given (yield)

Итак, теперь мы можем изменить наш метод each для использования enum_for, который будет возвращать объект Enumerator, когда блок не задан:

class Fruit
  include Enumerable

  attr_accessor :kinds

  def initialize
    @kinds = %w(apple orange pear banana)
  end

  def each
    return enum_for(:each) unless block_given?
    @kinds.each { |kind| yield kind }
  end
end

f = Fruit.new
f.each
 => #<Enumerator: #<Fruit:0x007ff70aa3b548 @kinds=["apple", "orange", "pear", "banana"]>:each> 

И теперь у нас есть экземпляр Enumerator, которым мы можем управлять с помощью нашего клиентского кода для последующего использования.

person Donato    schedule 28.10.2018