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

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

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

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

вместо

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

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

Ответ 1

Большинство встроенных методов, которые принимают блок, возвращают перечислитель в случае отсутствия блока (например, 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

Ответ 2

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

p = "hello".enum_for(:each_char)

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

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

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

p = "hello".enum_for(:each_char)
loop do
    puts p.next
end

Ответ 3

Скажем, мы хотим взять массив ключей и массив значений и сшить их в 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?

Ответ 4

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

В вашем втором примере вы вызываете 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"]

Ответ 5

Это отлично подходит для больших или бесконечных объектов-генераторов. Например, следующее будет давать вам перечислитель для всего числа Фибоначчи, от 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 без необходимости связываться с Fiber s.

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

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]