Перечислитель как бесконечный генератор в Ruby
Я читаю один ресурс, объясняющий, как Enumerators можно использовать в качестве генераторов, что в качестве примера:
triangular_numbers = Enumerator.new do |yielder|
number = 0
count = 1
loop do
number += count
count += 1
yielder.yield number
end
end
print triangular_numbers.next, " "
print triangular_numbers.next, " "
print triangular_numbers.next, " "
Я не понимаю цели yielder
здесь, какое значение он принимает и как этот код выполняется параллельно с остальной частью программного кода.
Выполнение начинается сверху и приостанавливается, вероятно, когда блок "дает" значение для моего кода.
Может кто-нибудь объяснить, как все это выполняется в глазах компилятора?
Ответы
Ответ 1
Я думаю, что нашел что-то интересное.
Эта статья: "Ruby 2.0 Work Hard, так что вы можете быть ленивы" Пэта Шонесси объясняет идеи оценки Eager и Lazy, а также объясняет, как это относится к "рамочным классам", таким как Enumerale, Generator или Yielder. Это в основном сосредоточено на объяснении того, как достичь LazyEvaluation, но все же, это довольно подробно.
Ruby 2.0 реализует ленивую оценку с помощью объекта Enumerator:: Lazy. Что делает особенным то, что он играет обе роли! Это перечислитель, а также содержит ряд перечислимых методов. Он вызывает каждого, чтобы получить данные из источника перечисления, и он дает данные остальной части перечисления. Поскольку Enumerator:: Lazy играет обе роли, вы можете связать их вместе, чтобы создать одно перечисление.
Это ключ к ленивой оценке в Ruby. Каждое значение из источника данных передается моему блоку, а затем результат немедленно передается по цепочке перечислений. Это перечисление не годится - метод Enumerator:: Lazy # collect не собирает значения в массив. Вместо этого каждое значение передается по очереди по цепочке объектов Enumerator:: Lazy посредством повторных выходов. Если бы я соединил цепочку вызовов или других методов Enumerator:: Lazy, каждое значение передавалось по цепочке из одного из моих блоков в следующий, по одному за раз.
Перечисляемый # сначала запускает итерацию, вызывая каждую из ленивых счетчиков и заканчивая итерацию, создавая исключение, когда оно имеет достаточные значения.
В конце концов, это ключевая идея ленивой оценки: функция или метод в конце цепочки вычислений запускает процесс выполнения, а поток программ работает обратно через цепочку вызовов функций, пока не получит только данные необходимых ему. Ruby достигает этого, используя цепочку объектов Enumerator:: Lazy.
Ответ 2
Yielder
- это всего лишь фрагмент кода, который возвращает значение и ждет следующего вызова.
Этого можно легко достичь, используя Ruby Fiber
Class. См. Следующий пример, который создает класс SimpleEnumerator
:
class SimpleEnumerator
def initialize &block
# creates a new Fiber to be used as an Yielder
@yielder = Fiber.new do
yield Fiber # call the block code. The same as: block.call Fiber
raise StopIteration # raise an error if there is no more calls
end
end
def next
# return the value and wait until the next call
@yielder.resume
end
end
triangular_numbers = SimpleEnumerator.new do |yielder|
number = 0
count = 1
loop do
number += count
count += 1
yielder.yield number
end
end
print triangular_numbers.next, " "
print triangular_numbers.next, " "
print triangular_numbers.next, " "
Я только что заменил Enumerator.new
в вашем коде на SimpleEnumerator.new
, и результаты совпадают.
Существует "легкий вес кооператива concurrency"; используя слова документации Ruby, где программист планирует, что должно быть сделано, другими словами, программист может приостановить и возобновить блок кода.
Ответ 3
Предположим, что мы хотим напечатать первые три треугольных числа. Наивная реализация заключалась бы в использовании функции:
def print_triangular_numbers steps
number = 0
count = 1
steps.times do
number += count
count += 1
print number, " "
end
end
print_triangular_numbers(3)
Недостатком здесь является то, что мы смешиваем логику печати с логикой подсчета. Если мы не хотим печатать цифры, это не полезно. Мы можем улучшить это, вместо чего присвоим числа блоку:
def triangular_numbers steps
number = 0
count = 1
steps.times do
number += count
count += 1
yield number
end
end
triangular_numbers(3) { |n| print n, " " }
Теперь предположим, что мы хотим напечатать несколько треугольных чисел, сделать некоторые другие вещи, а затем продолжить их печать. Опять же, наивное решение:
def triangular_numbers steps, start = 0
number = 0
count = 1
(steps + start).times do
number += count
yield number if count > start
count += 1
end
end
triangular_numbers(4) { |n| print n, " " }
# do other stuff
triangular_numbers(3, 4) { |n| print n, " " }
Это имеет тот недостаток, что каждый раз, когда мы хотим возобновить печать треугольных чисел, нам нужно начинать с нуля. Неэффективное! Нам нужен способ запомнить, где мы остановились, чтобы мы могли возобновить работу позже. Переменные с proc делают легкое решение:
number = 0
count = 1
triangular_numbers = proc do |&blk|
number += count
count += 1
blk.call number
end
4.times { triangular_numbers.call { |n| print n, " " } }
# do other stuff
3.times { triangular_numbers.call { |n| print n, " " } }
Но это один шаг вперед и два шага назад. Мы можем легко возобновить, но нет инкапсуляции логики (мы могли бы случайно изменить number
и разрушить все!). Нам действительно нужен объект , где мы можем сохранить состояние. Это именно то, что Enumerator
для.
triangular_numbers = Enumerator.new do |yielder|
number = 0
count = 1
loop do
number += count
count += 1
yielder.yield number
end
end
4.times { print triangular_numbers.next, " " }
# do other stuff
3.times { print triangular_numbers.next, " " }
Так как блоки являются замыканиями в Ruby, loop
запоминает состояние number
и count
между вызовами. Это то, что заставляет казаться, что счетчик работает параллельно.
Теперь мы перейдем к игроку. Обратите внимание, что он заменяет blk.call number
в предыдущем примере, где мы использовали proc. blk.call
работал, но он был негибким. В Ruby вам не всегда нужно предоставлять счетчикам блоки. Иногда вы просто хотите перечислить один шаг за раз или перечислители цепей вместе, в тех случаях, когда ваш перечислитель просто передает значение блоку, это неудобно. Enumerator
упрощает запись счетчиков с помощью агностического интерфейса Enumerator::Yielder
. Когда вы даете значение yielder (yielder.yield number
или yielder << number
), вы указываете перечислитель "Всякий раз, когда кто-то запрашивает следующее значение (будь то в блоке с next
, each
) или передается прямо к другому перечислителю), дайте им это". Ключевое слово yield
просто не вырезало бы его здесь, потому что оно предназначено только для того, чтобы уступать значения блокам.
Ответ 4
В Ruby Cookbook я нашел приятный краткий ответ:
https://books.google.com/books?id=xBmkBwAAQBAJ&pg=PT463&lpg=PT463&dq=upgrade+ruby+1.8+generator&source=bl&ots=yyVBoNUhNj&sig=iYXXR_8QqVMasFnS53sbUzGAbTc&hl=en&sa=X&ei=fOM-VZb0BoXSsAWulIGIAw&ved=0CFcQ6AEwBw#v=onepage&q=upgrade%20ruby%201.8%20generator&f=false
Это показывает, как создать стиль Ruby 1.8 Generator
с использованием класса Ruby 2.0 + Enumerator
.
my_array = ['v1', 'v2']
my_generator = Enumerator.new do |yielder|
index = 0
loop do
yielder.yield(my_array[index])
index += 1
end
end
my_generator.next # => 'v1'
my_generator.next # => 'v2'
my_generator.next # => nil