在 Scala 中,从初始对象和生成下一个对象的函数创建 O(1) 内存 Iterable

Posted

技术标签:

【中文标题】在 Scala 中,从初始对象和生成下一个对象的函数创建 O(1) 内存 Iterable【英文标题】:Creating an O(1)-memory Iterable from an initial object and a function which generates the next object, in Scala 【发布时间】:2011-04-16 14:13:10 【问题描述】:

我想要一种方便的方法来生成Iterable,给定一个初始对象和一个从当前对象生成下一个对象的函数,它消耗 O(1) 内存(即,它不缓存旧结果;如果要第二次迭代,则必须再次应用该函数。

似乎没有对此的库支持。在 Scala 2.8 中,方法 scala.collection.Iterable.iterate 具有签名

def iterate [A] (start: A, len: Int)(f: (A) ⇒ A) : Iterable[A]

所以它要求您提前指定您感兴趣的迭代函数应用程序的数量,我对文档的理解是Iterable.iterate 实际上会立即计算所有这些值。另一方面,scala.collection.Iterator.iterate 方法有签名

def iterate [T] (start: T)(f: (T) ⇒ T) : Iterator[T]

看起来不错,但我们只得到一个Iterator,它不能提供mapfilter 和朋友的所有便利。

有没有方便的库方法来生成我想要的东西?

如果没有,

有人可以建议使用“口语”Scala 代码来执行此操作吗?

总而言之,给定一个初始对象 a: A 和一个函数 f: A => A,我想要一个生成 a, f(a), f(f(a)), ... 并使用 O(1) 的 TraversableLike(例如,可能是一个 Iterable)内存,mapfilter 等函数也返回内存中的 O(1)。

【问题讨论】:

一个“线索”:多阅读 API,我开始怀疑一个好的答案会提到 TraversableViewLike,但我也越来越难过。 迭代器 map、filter和friends...你确定他们使用的不仅仅是常量内存吗? 是的,map和filter等在Iterator上都有,不要尝试强制Iterator这样的傻事。但是Iterable 会更方便;为什么我不应该期望能够使用tail(无论何时调用iterator,都应该通过调用next删除第一个元素,然后再交回Iterator)等? (事实上​​,当我试图将我的代码从期待 Iterables 切换到 Iterators 时,这是我必须解决的问题。) 【参考方案1】:

Stream 会做你想做的,只是不要抓住细胞;只对值进行迭代。

Stream 固有地缓存它们计算的每个值,这是一个可悲的普遍误解。

如果你这样写:

val s1: Stream[Thing] = initialValue #:: «expression computing next value»

那么确实保留了流产生的每个值,但这不是必需的。如果你写:

def s2: Stream[Thing] = initialValue #:: «expression computing next value»

如果调用者只是迭代流的值,但不记得 Stream 值本身(特别是它的任何 cons 单元格),则不会发生不需要的保留。当然,在这个公式中,每次调用都会从一个固定的初始值开始创建一个新的Stream。没必要:

def s3(start: Thing): Stream[Thing] = start #:: «expression computing next value»

您需要注意的一件事是将Stream 传递给方法。这样做将捕获方法参数中传递的流的头部。解决此问题的一种方法是使用尾递归代码处理流。

【讨论】:

我不明白——我需要能够将此对象传递给其他消费者;也就是说,未知的其他代码实际上会进行迭代。如果不传递对 Stream 头部的引用,我不知道如何做到这一点。 这是一个限制。正如我所说,您必须构建代码以通过尾调用优化链传递Stream。但是那个“未知”代码知道它得到了一个Stream,所以它知道它不能保留对其(流)cons 单元格的引用。 不,这真的不行。为什么“未知”代码会知道什么?如果其他人调用我的代码,为什么他们不将返回值视为Iterable @Scott Morrison:丹尼尔的答案(这个问题中最新的)不合适吗? 否:请参阅我对他的回答的评论。也许我没有足够清楚地解释我想要什么,应该再试一个新问题。【参考方案2】:

Iterator.iterate 带过滤器的演示:

object I 
  def main(args:Array[String]) 
    val mb = 1024 * 1024
    val gen = Iterator.iterate(new Array[Int](10 * mb))arr => 
      val res = new Array[Int](10 * mb)
      arr.copyToArray(res)
      println("allocated 10mb")
      res(0) = arr(0) + 1 // store iteration count in first elem of new array
      res
    
    // take 1 out of 100
    val gen2 = gen filter (arr => arr(0) % 100 == 0) 
    // print first 10 filtered
    gen2.take(10).foreach  arr => println("filtered " + arr(0))  
  

(这在 REPL 中可能不起作用,因为 PRINT 步骤可能会干扰内存管理)

JAVA_OPTS="-Xmx128m" scala -cp classes I 将显示过滤有效并且是惰性的。如果它不是在常量内存中完成,会导致堆错误(因为它分配了 900*10mb 之类的东西)。

使用JAVA_OPTS="-Xmx128m -verbose:gc" scala -cp classes I 查看垃圾回收事件。

【讨论】:

感谢详细信息让我相信一切都是 O(1)。我去试试这个。【参考方案3】:

迭代器正是你想要的。而且迭代器确实有map,filter,takeWhile和许多其他方法,在内存中是O(1)。我认为内存中没有另一种O(1)的集合类型。

【讨论】:

【参考方案4】:
val it = new Iterable[Int] 
  def iterator = Iterator.iterate(0)(_+1)
  override
  def toString: String = "Infinite iterable"

不要在 REPL 上尝试(除非将其嵌入到对象或类中),因为 REPL 会尝试打印它,并且它不使用 toString

【讨论】:

在trunk中打印“Infinite iterable”。 至少据我了解,it map _ + 1 take 5 不会终止,因为map 会尝试强制Iterable @Scott Iterable 不一定是懒惰的。除非您花时间使所有方法变得懒惰,否则就是提供的。但是,it.view map _ + 1 take 5 会起作用,所以我不明白为什么要担心它。

以上是关于在 Scala 中,从初始对象和生成下一个对象的函数创建 O(1) 内存 Iterable的主要内容,如果未能解决你的问题,请参考以下文章

scala设计者为啥要提供package object

如何在 Arduino 程序中正确初始化 C++ 对象?

Scala:如何使用默认值初始化对象

scala 类,伴生对象

Scala 系列—— 类和对象

jooq + scala 代码生成:对象 AbstractKeys 中的方法 createIndex 无法在对象 org.jooq.impl.AbstractKeys 中访问