Ruby-эквивалент ключевого слова C# yield или создание последовательностей без предварительного выделения памяти

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

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.

ИЗМЕНИТЬ

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


person Drew Noakes    schedule 17.02.2010    source источник


Ответы (4)


Он поддерживается 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() }
person Matthew Flaschen    schedule 17.02.2010
comment
@ Мэтью, это выглядит именно так, как я хочу. Жаль, что это Ruby 1.9, так как я сейчас на 1.8.7. Посмотрю, смогу ли я обновить. Если вы знаете о подходе до 1.9, я хотел бы услышать его. - person Drew Noakes; 18.02.2010
comment
Согласно этой статье rubyinside.com/ruby-187-released-912.html поддержка последовательности Enumerator была перенесена обратно в 1.8.7. Счастливые дни. - person Drew Noakes; 19.02.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. Так что это точно эквивалентно вашему примеру на С#.

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

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

person sepp2k    schedule 17.02.2010
comment
Этот ответ требует блока. В C# нет концепции блоков, а оператор yield в C# делает что-то совсем другое. Есть ли способ создать произвольную последовательность в качестве возвращаемого значения из метода? Преимущество наличия его в качестве экземпляра заключается в том, что им можно манипулировать, фильтровать, объединять, отображать и т. д. - person Drew Noakes; 18.02.2010
comment
Я обновил свой вопрос, чтобы быть более явным. Я думаю, что разница в значении ключевого слова yeild между языками вызвала некоторую путаницу. - person Drew Noakes; 18.02.2010
comment
@Drew: для этого ответа требуется блок. Нет, это не так. Посмотрите на мой пример использования - блока нет. Я могу сделать 10000.my_times.first, чтобы получить 0 (первый элемент перечислителя), или 10000.my_times.to_a, чтобы получить массив содержимого перечислителя. Или я мог бы вызвать для него любой другой метод Enumerable. my_times (без блока) возвращает Enumerable, который содержит полученные элементы. Это именно то, о чем вы просили. - person sepp2k; 18.02.2010

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

Быстрый поиск в Google обнаружил бета-версию ruby-библиотеки. Посмотрите, если это то, что вы хотите.

person Pontus Gagge    schedule 17.02.2010
comment
Кто-нибудь, пожалуйста, поправьте меня, если я ошибаюсь, но я считаю, что Enumerator в любом случае обеспечивает ленивое выполнение? - person Shadowfirebird; 18.02.2010

C# вырезал ключевое слово «yield» прямо из Ruby — см. Реализация итераторов здесь больше.

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

person glenatron    schedule 17.02.2010
comment
Скорее всего, не. Спецификация C# 2.0 была завершена в декабре 2002 года. Ruby 1.9.0 был выпущен в декабре 2007 года. Более того, если C# и скопировал его откуда-то, то это был CLU, который восходит к 1975 году. - person Matthew Flaschen; 17.02.2010
comment
@Matthew Flaschen: у Руби было yield с 90-х. Он не был представлен в 1.9. Однако он сильно отличается от C#, хотя оба связаны с итерацией. Выход Ruby — это просто сахар для вызова переданного блока, в то время как ключевое слово C# возвращает сам итератор. Так, например, первый пример в этом документе итераторов (threeTimes) не будет реализован с использованием yield в C#. Версия C#, похоже, пришла из Python. - person Chuck; 17.02.2010
comment
Ах хорошо. Я неправильно понял статью в Википедии. Но моя главная мысль заключалась в том, что неправдоподобно говорить, что C# взял ключевое слово yield из Ruby, когда доходность CLU опережает их обоих на десятилетия. - person Matthew Flaschen; 17.02.2010
comment
Мац несколько раз прямо заявлял, что итераторы Ruby прямо из CLU. - person Jörg W Mittag; 17.02.2010
comment
Я просто привел сглаживание массива в качестве примера. Во всяком случае, я подозреваю, что сглаженный массив приводит к всплеску памяти элементов X * Y, чего я пытаюсь избежать. И да, команда yield C# совершенно другая — это было первое, что я проверил. - person Drew Noakes; 18.02.2010
comment
Глядя на комментарий @Matthew выше и его ответ, может случиться так, что есть путаница в методе yield и ключевом слове. Кажется, метод yield был добавлен в 1.9 вместе с классом Enumerator. - person Drew Noakes; 18.02.2010