Метод ленивого сглаживания на основе Ruby Enumerator

У Майкла Харрисона есть отличный пост о ленивом перечислителе в Ruby, где представлена ​​реализация lazy_select и lazy_map. Мне интересно, должна ли следующая реализация lazy_flatten иметь специальную обработку для чего-либо, кроме типов Enumerator и Enumerable.

class Enumerator

  def lazy_flatten
    Enumerator.new do |yielder|
      self.each do |value|
        if value.kind_of? Enumerator
          value.lazy_flatten.each do |v|
            yielder.yield v
          end
        elsif value.kind_of? Enumerable
          value.flatten.each do |v|
            yielder.yield v
          end
        else
          yielder.yield value
        end
      end
    end
  end

end

person Sim    schedule 09.04.2012    source источник


Ответы (2)


  1. Мне это не кажется ленивым, так как внизу вы все еще исполняете старые (неленивые) flatten.
  2. Enumerator это Enumerable, поэтому я думаю, что вам не нужно обрабатывать его отдельно.
  3. Я ожидаю, что lazy_flatten будет методом Enumerable.

Вот как бы я это реализовал:

module Enumerable
  def lazy_flatten
    Enumerator.new do |yielder|
      each do |element|
        if element.is_a? Enumerable
          element.lazy_flatten.each do |e|
            yielder.yield(e)
          end
        else
          yielder.yield(element)
        end
      end
    end
  end
end
person Mladen Jablanović    schedule 09.04.2012

Обратите внимание, что в Ruby 2.0+ вам не нужно этого делать, вы можете просто использовать Enumerable#lazy, который возвращает Enumerator::Lazy.

По непонятным мне причинам Lazy не имеет flatten, но имеет flat_map, так что в принципе вы можете просто использовать flat_map с функция идентификации.

module Enumerable
  def lazy_flatten
    self.lazy.flat_map { |x| x }
  end
end

Lazy#flat_map в основном занимается декомпозицией любых декомпозируемых элементов, но не совсем -- из документы:

Значение x, возвращаемое блоком, декомпозируется, если выполняется одно из следующих условий:

  1. x отвечает как на each, так и на force, что означает, что x является ленивым перечислителем.
  2. x представляет собой массив или отвечает на to_ary.

Обратите внимание, что to_ary не является методом Enumerable, предположительно для предотвращения неявных преобразований от бесконечных последовательностей к массивам. Это означает, например, что если вы попытаетесь lazy_flatten что-то, что содержит Set или Range с приведенным выше кодом, это (аргументировано, см. ниже) не будет работать:

a = [[1, 2, 3], Set[4, 5], 6, 7..8]
# => [[1, 2, 3], #<Set: {4, 5}>, 6, 7..8] 
f = a.lazy_flatten
# => #<Enumerator::Lazy: #<Enumerator::Lazy: [[1, 2, 3], #<Set: {4, 5}>, 6, 7..8]>:flat_map>  
f.to_a
# => [1, 2, 3, #<Set: {4, 5}>, 6, 7..8]

Однако это то же самое, что и поведение Array#flatten:

a.flatten
# => [1, 2, 3, #<Set: {4, 5}>, 6, 7..8]

(Хотя Array#flatten не будет обнаруживать и разлагать ленивые счетчики, а Lazy#flat_map будет.)

Принимая во внимание, что код OP или код в ответе Младена Яблановича разложит Set и Range:

f = a.lazy_flatten # (M.J.'s code)
# => #<Enumerator: #<Enumerator::Generator:0x00007fd819c166c0>:each>
f.to_a
# => [1, 2, 3, 4, 5, 6, 7, 8]

Однако этот код также будет повторяться бесконечно, если будет передано что-то, что включает бесконечную последовательность:

a = [[1, 2, 3], Set[4, 5], 6, 7..8, 9..Float::INFINITY]
# => [[1, 2, 3], #<Set: {4, 5}>, 6, 7..8, 9..Infinity]
f = a.lazy_flatten # (M.J.'s code)
# => #<Enumerator: #<Enumerator::Generator:0x00007fd819a73d18>:each>
f.to_a
# => spins at 100% CPU for a while and eventually runs out of memory

Если вы считаете, что это функция, а не ошибка, одним из подходов будет изменение реализации на основе flat_map для преобразования любых перечислений, которые она находит, в ленивые:

module Enumerable
  def lazy_flatten
    self.lazy.flat_map do |x|
      x.respond_to?(:lazy) ? x.lazy : x
    end
  end
end

Это работает даже для вложенных ленивых перечислений, поскольку Lazy#lazy достаточно умен, чтобы возвращать себя.

person David Moles    schedule 25.01.2021