基于Ruby枚举器的惰性扁平化方法

Ruby Enumerator-based lazy flatten method

Michael Harrison在Ruby中的懒惰枚举器上有一篇不错的文章,提供了lazy_selectlazy_map的实现。我想知道以下lazy_flatten的实现是否应该对EnumeratorEnumerable类型以外的任何内容进行特殊处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

  • 这对我来说似乎并不懒,因为您仍然在下面执行旧的(非惰性)flatten
  • EnumeratorEnumerable,所以我认为您不需要单独处理它。
  • 我希望lazy_flattenEnumerable的方法。
  • 这是我的实现方式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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

    请注意,在Ruby 2.0中,您无需执行此操作,只需使用Enumerable#lazy,它会返回Enumerator::Lazy

    由于我不清楚的原因,Lazy没有flatten,但是它具有flat_map,因此原则上您可以仅将flat_map与身份功能一起使用。

    1
    2
    3
    4
    5
    module Enumerable
      def lazy_flatten
        self.lazy.flat_map { |x| x }
      end
    end

    Lazy#flat_map主要负责分解所有可分解元素,但并非完全如此-从docs:

    A value x returned by block is decomposed if either of the following conditions is true:

  • x responds to both each and force, which means that x is a lazy enumerator.
  • x is an array or responds to to_ary.
  • 请注意,to_ary不是Enumerable的方法,大概是要阻止从无限序列到数组的隐式转换。例如,这意味着,如果您尝试使用上述代码尝试lazy_flatten包含SetRange的内容,则它(arguaby,请参见下文)将不起作用:

    1
    2
    3
    4
    5
    6
    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

    的行为相同

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

    (尽管Array#flatten不会检测和分解惰性枚举器,而Lazy#flat_map可以。)

    鉴于OP的代码或Mladen Jablanovi中的代码?\\的答案将分解SetRange

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

    但是,如果传递的代码包含无限序列,该代码也将无限迭代:

    1
    2
    3
    4
    5
    6
    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的实现,以将找到的所有可枚举转换为惰性:

    1
    2
    3
    4
    5
    6
    7
    module Enumerable
      def lazy_flatten
        self.lazy.flat_map do |x|
          x.respond_to?(:lazy) ? x.lazy : x
        end
      end
    end

    这对于嵌套的惰性枚举也有效,因为Lazy#lazy足够聪明以返回自身。