Рубиновый эквивалент ключевого слова 'yield' в C # или создание последовательностей без предварительного выделения памяти - PullRequest
7 голосов
/ 17 февраля 2010

В C # вы можете сделать что-то вроде этого:

public IEnumerable<T> GetItems<T>()
{
    for (int i=0; i<10000000; i++) {
        yield return i;
    }
}

Возвращает перечисляемую последовательность из 10 миллионов целых чисел без выделения коллекции в памяти такой длины.

Есть ли способ сделать эквивалентную вещь в Ruby? Конкретный пример, с которым я пытаюсь разобраться, - это сглаживание прямоугольного массива в последовательность значений для перечисления. Возвращаемое значение не обязательно должно быть Array или Set, а скорее какой-то последовательностью, которую можно повторять / перечислять только по порядку, а не по индексу. Следовательно, вся последовательность не должна быть распределена в памяти одновременно. В .NET это IEnumerable и IEnumerable<T>.

Любое разъяснение терминологии, используемой здесь, в мире Ruby, было бы полезно, так как я более знаком с терминологией .NET.

EDIT

Возможно, мой первоначальный вопрос не был достаточно ясен - я думаю, что тот факт, что yield имеет очень разные значения в C #, а Ruby является причиной путаницы здесь.

Мне не нужно решение, которое требует, чтобы мой метод использовал блок. Я хочу решение, которое имеет фактическое возвращаемое значение. Возвращаемое значение обеспечивает удобную обработку последовательности (фильтрация, проекция, объединение, архивирование и т. Д.).

Вот простой пример того, как я могу использовать get_items:

things = obj.get_items.select { |i| !i.thing.nil? }.map { |i| i.thing }

В C # любой метод, возвращающий IEnumerable, который использует yield return, заставляет компилятор генерировать конечный автомат за кулисами, который обслуживает это поведение. Я подозреваю, что чего-то подобного можно достичь, используя продолжения Ruby, но я не видел примера и не совсем ясно, как это можно сделать.

Действительно кажется возможным, что я мог бы использовать Enumerable для достижения этой цели. Простым решением для нас будет Array (который включает в себя модуль Enumerable), но я не хочу создавать промежуточную коллекцию с N элементами в памяти, когда можно просто предоставить их лениво и вообще избежать скачка памяти .

Если это все еще не имеет смысла, рассмотрите приведенный выше пример кода. get_items возвращает перечисление, для которого вызывается select. То, что передается в select, является экземпляром, который знает, как предоставить следующий элемент в последовательности, когда это необходимо. Важно отметить, что вся коллекция предметов еще не рассчитана. Только когда select понадобится предмет, он попросит его, и скрытый код в get_items начнет действовать и предоставит его. Эта лень несет по цепочке, так что select вытягивает следующий элемент из последовательности, только когда map запрашивает его. Таким образом, длинная цепочка операций может выполняться одновременно для одного элемента данных. Фактически, код, структурированный таким образом, может даже обрабатывать бесконечную последовательность значений без каких-либо ошибок памяти.

Итак, этот тип лени легко кодируется в C #, и я не знаю, как это сделать в Ruby.

Надеюсь, это будет понятнее (я постараюсь не писать вопросы в 3 часа ночи в будущем.)

Ответы [ 4 ]

14 голосов
/ 17 февраля 2010

Поддерживается Enumerator начиная с Ruby 1.9 (и с портированием на 1.8.7) См. Генератор: Ruby .

Пример клише:

fib = Enumerator.new do |y|
  y.yield i = 0
  y.yield j = 1
  while true
    k = i + j
    y.yield k
    i = j
    j = k
  end
end

100.times { puts fib.next() }
5 голосов
/ 17 февраля 2010

Ваш конкретный пример эквивалентен 10000000.times, но давайте на минутку предположим, что метод times не существует, и вы хотите реализовать его самостоятельно, он будет выглядеть так:

class Integer
  def my_times
    return enum_for(:my_times) unless block_given?
    i=0
    while i<self
      yield i
      i += 1
    end
  end
end

10000.my_times # Returns an Enumerable which will let
               # you iterate of the numbers from 0 to 10000 (exclusive)

Редактировать: Чтобы уточнить мой ответ немного:

В приведенном выше примере my_times может (и есть) использоваться без блока, и он вернет объект Enumerable, который позволит вам перебирать числа от 0 до n. Так что это в точности соответствует вашему примеру в C #.

Это работает с использованием метода enum_for. Метод enum_for принимает в качестве аргумента имя метода, который выдаст несколько элементов. Затем он возвращает экземпляр класса Enumerator (который включает в себя модуль Enumerable), который при повторной итерации выполнит данный метод и выдаст вам элементы, которые были получены методом. Обратите внимание, что если вы перебираете только первые x элементов перечислимого, метод будет выполняться только до тех пор, пока не будет получено x элементов (т. Е. Будет выполнено столько, сколько необходимо для метода), и если вы перебираете перечисляемое дважды, метод будет выполнен дважды.

В 1.8.7+ стало определяться методы, которые выдают элементы, так что при вызове без блока они возвращают перечислитель, который позволит пользователю лениво выполнять итерации по этим элементам. Это делается путем добавления строки return enum_for(:name_of_this_method) unless block_given? в начало метода, как я делал в моем примере.

1 голос
/ 17 февраля 2010

Не имея большого опыта в рубине, то, что C # делает в yield return, обычно называют ленивым вычислением или ленивым выполнением : предоставление ответов только по мере необходимости. Речь идет не о распределении памяти, а об отсрочке вычислений до момента, когда это действительно необходимо, выражается способом, аналогичным простому линейному выполнению (а не базовому итератору с сохранением состояния).

Быстрый Google включил библиотеку ruby ​​ в бета-версии. Посмотри, если ты этого хочешь.

0 голосов
/ 17 февраля 2010

C # вырвал ключевое слово yield из Ruby - см. Реализация итераторов здесь для получения дополнительной информации.

Что касается вашей реальной проблемы, у вас предположительно есть массив массивов, и вы хотите создать одностороннюю итерацию по всей длине списка? Возможно, стоит взглянуть на array.flatten в качестве отправной точки - если производительность в порядке, то вам, вероятно, не нужно заходить слишком далеко.

...