Подтвердить что ты не робот

Перечислитель как бесконечный генератор в 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 здесь, какое значение он принимает и как этот код выполняется параллельно с остальной частью программного кода.

Выполнение начинается сверху и приостанавливается, вероятно, когда блок "дает" значение для моего кода.

Может кто-нибудь объяснить, как все это выполняется в глазах компилятора?

4b9b3361

Ответ 1

Я думаю, что нашел что-то интересное.

Эта статья: "Ruby 2.0 Work Hard, так что вы можете быть ленивы" Пэта Шонесси объясняет идеи оценки Eager и Lazy, а также объясняет, как это относится к "рамочным классам", таким как Enumerale, Generator или Yielder. Это в основном сосредоточено на объяснении того, как достичь LazyEvaluation, но все же, это довольно подробно.


Источник оригинала: 'Ruby 2.0 работает так, что вы можете быть ленивы' от Pat Shaughnessy

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