如何在Ruby中获取惰性数组?

31

如何在 Ruby 中获取懒加载数组?

在 Haskell 中,我可以使用 [1..] 来表示一个无限列表,它会在需要时进行惰性生成。我还可以做一些类似于 iterate (+2) 0 的操作,它会将我给定的任何函数应用于生成惰性列表,也就是会给我所有的偶数。

我相信在 Ruby 中也能够实现类似的操作,但是我似乎无法找到方法。


2
关于惰性数组:数组与列表有显著的不同。实现允许无限数组的惰性数组会具有可怕的运行时属性。 - sepp2k
9个回答

42

使用 Ruby 1.9,你可以使用 Enumerator 类。这是来自文档的一个示例:

  fib = Enumerator.new { |y|
    a = b = 1
    loop {
      y << a
      a, b = b, a + b
    }
  }

  p fib.take(10) #=> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

此外,这是一个不错的技巧:

  Infinity = 1.0/0

  range = 5..Infinity
  p range.take(10) #=> [5, 6, 7, 8, 9, 10, 11, 12, 13, 14]

然而,这个只适用于连续的值。


6
它们不必连续:(10..100).step(20).take(5) #=> [10, 20, 30, 40, 50]。 - horseyguy
2
在 Ruby 1.8 中,你可以使用 require 'backports' 来获取这个功能。 :-) - Marc-André Lafortune
2
请注意,执行 fib.map {|x| x+1}.take(10) 将不起作用,因为 map 会尝试创建一个数组。还要注意,如果您两次执行 fib.take(10),那么元素将被计算两次(与惰性列表不同,惰性列表在计算后将元素保留在内存中)。因此,这并不完全等同于惰性列表。 - sepp2k
2
为了执行 fib.map 的 Enumerator 等效操作,你需要使用 fib.enum_for(:map) - Chuck
1
点赞。Ruby中的Enumerator和Infinity对我来说都是新东西。今天我第一次查找了一些Haskell的内容,所以很高兴在Ruby中看到类似的语法来表达相似的概念(例如 take)。 - KChaloux
显示剩余2条评论

23

最近在ruby trunk中添加了Enumerable::Lazy,我们将在ruby 2.0中看到它。

a = data.lazy.map(&:split).map(&:reverse)

该方法不会立即被执行。
其结果是 Enumerable::Lazy 类的实例,可以进一步进行懒惰链接。如果您想要获取实际结果,请使用#to_a#take(n)#take现在也是懒惰的,请使用#to_a#force等)。
如果您想了解更多关于这个主题和我的 C 补丁,请参阅我的博客文章Ruby 2.0 Enumerable::Lazy


6

惰性序列(自然数):

Inf = 1.0/0.0
(1..Inf).take(3) #=> [1, 2, 3]

懒惰的范围(偶数):

(0..Inf).step(2).take(5) #=> [0, 2, 4, 6, 8]

请注意,您还可以使用一些方法扩展Enumerable,以便更方便地处理惰性范围等内容:

module Enumerable
  def lazy_select
    Enumerator.new do |yielder|
      each do |obj|
        yielder.yield(obj) if yield(obj)
      end
    end
  end
end

# first 4 even numbers
(1..Inf).lazy_select { |v| v.even? }.take(4)

output:
[2, 4, 6, 8]

更多信息请参见:http://banisterfiend.wordpress.com/2009/10/02/wtf-infinite-ranges-in-ruby/ 此外,在此处可以找到Enumerator类的lazy_map和lazy_select实现:http://www.michaelharrison.ws/weblog/?p=163

请注意,与枚举器解决方案一样,您无法执行诸如 infinite_range.filter {|x| f(x)}.take(5) 这样的操作,因此它不像惰性列表那样运行。 - sepp2k
@sepp2k,添加了一个链接到网站,该网站具有lazy_select等的实现,适用于Enumerator类。 - horseyguy

4

2
我很惊讶还没有人对这个问题进行恰当的回答
所以,最近我发现了这个方法Enumerator.produce,与.lazy结合使用可以完全实现你所描述的功能,但以一种Ruby风格的方式。
举个例子:
Enumerator.produce(0) do  
   _1 + 2
end.lazy
  .map(&:to_r) 
  .take(1_000)
  .inject(&:+)
# => (999000/1)

def fact(n)
  = Enumerator.produce(1) do  
    _1 + 1
  end.lazy
  .take(n)
  .inject(&:*)

fact 6 # => 720 

1
这将循环到无限大:

0.step{|i| puts i}

这将以两倍于无限循环速度的方式循环:

0.step(nil, 2){|i| puts i}

只要你愿意,这将会无限循环(结果为枚举器)。

table_of_3 = 0.step(nil, 3)

1

正如我在评论中所说的那样,实现诸如惰性数组之类的东西是不明智的。

在某些情况下,使用 Enumerable 可以很好地工作,但与惰性列表有所不同:像 map 和 filter 这样的方法不会被惰性评估(因此它们不适用于无限可枚举对象),并且已经计算过的元素不会被存储,因此如果您两次访问一个元素,则会计算两次。

如果您想要在 Ruby 中获得与 Haskell 的惰性列表完全相同的行为,则可以使用 lazylist gem 实现惰性列表。


0
正确答案已经确定了“懒惰”方法,但提供的示例不太有用。我将给出一个更好的例子,说明何时适合在数组中使用lazy。如上所述,lazy被定义为模块Enumerable的实例方法,它可以作用于实现Enumerable模块的对象(例如数组-[].lazy)或枚举器,这些枚举器是可枚举模块中迭代器的返回值(例如each_slice-[].each_slice(2).lazy)。请注意,在可枚举模块中,一些实例方法返回更原始的值,如true或false,一些返回集合,如数组,一些返回枚举器。如果没有给出块,则某些返回枚举器。

但对于我们的例子,IO类还具有一个迭代器each_line,它返回一个枚举器,因此可以与“lazy”一起使用。返回枚举器的美妙之处在于,它实际上并不会将它正在处理的集合(例如大型数组)加载到内存中。相反,它有一个指向集合的指针,然后存储算法(例如each_slice(2)),当您想要使用to_a之类的东西处理集合时,它将在该集合上使用该算法。例如。

如果你正在使用枚举器来获得更高的性能,现在你可以将lazy附加到枚举器上。这样,你就不需要遍历整个集合来匹配条件了:

file.each_line.select { |line| line.size == 5 }.first(5)

您可以调用lazy:

file.each_line.lazy.select { |line| line.size == 5 }.first(5)

如果我们正在扫描一个大型文本文件以查找前5个匹配项,那么一旦找到这5个匹配项,就没有必要继续执行。因此,懒惰的特性适用于任何类型的可枚举对象。

-4

Ruby的数组会根据需要动态扩展。您可以应用块来返回诸如偶数之类的内容。

array = []
array.size # => 0
array[0] # => nil
array[9999] # => nil
array << 1
array.size # => 1
array << 2 << 3 << 4
array.size # => 4

array = (0..9).to_a
array.select do |e|
  e % 2 == 0
end

# => [0,2,4,6,8]

这有帮助吗?


2
问题不是关于惰性生成对象吗?正确的解决方案不是关于数组(在Ruby中总是完全确定的),而是关于其他类型的Enumerable,就像Sanjana的答案一样。 - Marc-André Lafortune
我刚开始学习 Ruby,我以为这是正确的。我会修正它。 -- 愿你的愿望成真。 - carlfilips

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接