В чем преимущество создания перечислимого объекта с использованием 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]