这种方法适用于简单情况并不奇怪:由于Haskell的惰性和垃圾回收,我们已经可以轻松使用无限数据结构。只要你的最终结果不依赖于一次获得所有值,它们可以在进行过程中被收集或者根本不需要被强制获取。
这就是为什么这个经典的斐波那契例子在常数¹空间中运行:一旦下两个条目被计算出来,列表中的先前条目就不再需要了,因此它们会随着进行而被收集——只要您没有任何其他指向该列表的指针。
fib n = fibs !! n
where fibs = 0 : 1 : zipWith (+) fibs (drop 1 fibs)
尝试使用不同的输入运行此函数,并查看内存使用情况。(使用
+RTS -s
运行。)
(如果您想要更详细的解释和图表,请查看我写的
this post。)
重点是,即使程序员可以获得无限量的信息,如果没有其他依赖于它,我们仍然可以对大部分信息进行垃圾回收。
完全相同的逻辑可以用于有效地实现FRP程序。
当然,一切都不是那么简单。在
fibs
示例中,如果我们有一个指向
fibs
列表开头的活动指针,内存使用量将会大幅上升。如果您有一个依赖于过多历史数据的计算,FRP也会发生同样的情况:这被称为
时间泄漏。
处理时间泄漏是实现高效、良好行为的FRP框架的未解决问题之一。提供表达力强的FRP抽象很困难,同时又不允许出现内存使用不当甚至灾难性的情况。我认为目前大多数方法都会提供抽象的FRP类型以及一组较不容易引起此类泄漏的操作;其中一种特别极端的形式是箭头化FRP,它完全没有提供行为/信号类型,而是用信号之间的变换(作为箭头)来表达所有内容。
我从未尝试过自己实现一个好的 FRP 系统,因此无法详细解释其中的问题。如果您对此主题更详细的内容感兴趣,一个很好的查找地点是 Conal Elliott 的博客,
this post 是一个很好的起点。您还可以查看他写的一些论文,比如
"Push-Pull Functional Reactive Programming" 以及其他关于这个主题的论文,包括一些关于箭头化 FRP 的论文,例如
"Functional Reactive Programming, Continued"(几乎随机选择)。
脚注:
¹ 这不是真正的常数空间,因为中间结果本身会变得更大。但它应该保持恒定数量的列表单元格在内存中。