В чем преимущество создания перечисляемого объекта с использованием to_enum в Ruby?

Зачем вам создавать прокси-ссылку на объект в Ruby, используя метод to_enum, а не просто используя объект напрямую? Я не могу придумать никакого практического применения для этого, пытаясь понять эту концепцию и где кто-то может ее использовать, но все примеры, которые я видел, кажутся очень тривиальными.

Например, зачем использовать:

"hello".enum_for(:each_char).map {|c| c.succ }

вместо

"hello".each_char.map {|c| c.succ }

Я знаю, что это очень простой пример, есть ли у кого-нибудь реальные примеры?


person Jason    schedule 19.05.2010    source источник


Ответы (5)


Большинство встроенных методов, которые принимают блок, возвращают перечислитель, если блок не указан (например, String#each_char в вашем примере). Для них нет причин использовать to_enum; оба будут иметь одинаковый эффект.

Однако некоторые методы не возвращают Enumerator. В этом случае вам может понадобиться использовать to_enum.

# How many elements are equal to their position in the array?
[4, 1, 2, 0].to_enum(:count).each_with_index{|elem, index| elem == index} #=> 2

Другой пример: Array#product, #uniq и #uniq! не принимали блок. В версии 1.9.2 это было изменено, но для обеспечения совместимости формы без блока не могут возвращать Enumerator. Можно по-прежнему «вручную» использовать to_enum для получения перечислителя:

require 'backports/1.9.2/array/product' # or use Ruby 1.9.2+
# to avoid generating a huge intermediary array:
e = many_moves.to_enum(:product, many_responses)
e.any? do |move, response|
  # some criteria
end 

Основное использование to_enum — это когда вы реализуете свой собственный итеративный метод. Обычно вы будете иметь в качестве первой строки:

def my_each
  return to_enum :my_each unless block_given?
  # ...
end
person Marc-André Lafortune    schedule 19.05.2010
comment
Это также полезно, если вы используете сторонние библиотеки, которые не возвращают перечислитель. - person Andrew Grimm; 01.12.2011

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

p = "hello".enum_for(:each_char)

p — внешний перечислитель. Одним из преимуществ внешних итераторов является то, что:

Внешние итераторы более гибкие, чем внутренние итераторы. Легко сравнить две коллекции на равенство с помощью внешнего итератора, например, но практически невозможно с внутренними итераторами…. Но, с другой стороны, внутренние итераторы проще в использовании, потому что они определяют логику итерации за вас. [Из книги Язык программирования Ruby, гл. 5.3]

Итак, с внешним итератором вы можете сделать, например:

p = "hello".enum_for(:each_char)
loop do
    puts p.next
end
person Marcin    schedule 20.05.2010

Допустим, мы хотим взять массив ключей и массив значений и сшить их в Hash:

С #to_enum

def hashify(k, v)
  keys = k.to_enum(:each)
  values = v.to_enum(:each)
  hash = []
  loop do
    hash[keys.next] = values.next
    # No need to check for bounds,
    # as #next will raise a StopIteration which breaks from the loop
  end
  hash
end

Без #to_enum:

def hashify(k, v)
  hash = []
  keys.each_with_index do |key, index|
    break if index == values.length
    hash[key] = values[index]
  end
  hash
end

Первый метод намного легче читать, не так ли? Ничуть не проще, но представьте, если бы мы каким-то образом манипулировали элементами из 3-х массивов? 5? 10?

person jakeonrails    schedule 16.03.2012
comment
Ужасный пример. Использование next убьет вашу производительность. Оба примера не обрабатывают разницу в размерах одинаково (один поднимается, другой останавливается). - person Marc-André Lafortune; 06.08.2014

Это не совсем ответ на ваш вопрос, но, надеюсь, он имеет значение.

Во втором примере вы вызываете each_char без передачи блока. При вызове без блока each_char возвращает Enumerator, поэтому ваши примеры на самом деле просто два способа сделать одно и то же. (т.е. оба приводят к созданию перечислимого объекта.)

irb(main):016:0> e1 = "hello".enum_for(:each_char)
=> #<Enumerator:0xe15ab8>
irb(main):017:0> e2 = "hello".each_char
=> #<Enumerator:0xe0bd38>
irb(main):018:0> e1.map { |c| c.succ }
=> ["i", "f", "m", "m", "p"]
irb(main):019:0> e2.map { |c| c.succ }
=> ["i", "f", "m", "m", "p"]
person mikej    schedule 19.05.2010

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

def fib_sequence
  return to_enum(:fib_sequence) unless block_given?
  yield 0
  yield 1
  x,y, = 0, 1
  loop { x,y = y,x+y; yield(y) }
end

to_enum эффективно позволяет вам писать это с помощью обычных yields без необходимости возиться с Fibers.

Затем вы можете нарезать его по своему усмотрению, и это будет очень эффективно использовать память, поскольку в памяти не будут храниться массивы:

module Slice
    def slice(range)
        return to_enum(:slice, range) unless block_given?
        start, finish = range.first, range.max + 1
        copy = self.dup
        start.times { copy.next }
        (finish-start).times { yield copy.next }
    end
end
class Enumerator
    include Slice
end

fib_sequence.slice(0..10).to_a
#=> [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
fib_sequence.slice(10..20).to_a                                                                                                                           
#=> [55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]
person PSkocik    schedule 21.02.2015