在 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
,它不能提供map
、filter
和朋友的所有便利。
有没有方便的库方法来生成我想要的东西?
如果没有,
有人可以建议使用“口语”Scala 代码来执行此操作吗?
总而言之,给定一个初始对象 a: A
和一个函数 f: A => A
,我想要一个生成 a, f(a), f(f(a)), ...
并使用 O(1) 的 TraversableLike
(例如,可能是一个 Iterable
)内存,map
、filter
等函数也返回内存中的 O(1)。
【问题讨论】:
一个“线索”:多阅读 API,我开始怀疑一个好的答案会提到TraversableViewLike
,但我也越来越难过。
迭代器有 map、filter和friends...你确定他们使用的不仅仅是常量内存吗?
是的,map和filter等在Iterator
上都有,不要尝试强制Iterator
这样的傻事。但是Iterable
会更方便;为什么我不应该期望能够使用tail
(无论何时调用iterator
,都应该通过调用next
删除第一个元素,然后再交回Iterator
)等? (事实上,当我试图将我的代码从期待 Iterable
s 切换到 Iterator
s 时,这是我必须解决的问题。)
【参考方案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的主要内容,如果未能解决你的问题,请参考以下文章
jooq + scala 代码生成:对象 AbstractKeys 中的方法 createIndex 无法在对象 org.jooq.impl.AbstractKeys 中访问