阅读FRP(功能反应式编程)我很惊讶它与标准命令式方法相比具有多么直观和逻辑性;然而有一件事让我感到困惑..计算机怎么不立即耗尽内存呢?
根据我从[here]收集的内容,在FRP中,值完整历史记录(过去,现在和未来)的值是第一类。这个概念立即在我的头脑中发出一声警告说,如果在没有立即从内存中清除值的过去的环境中使用它,它必须快速耗尽你的记忆非常。
阅读[Fran],我注意到有几个例子递归定义了没有终止条件的函数。如果函数永远不会终止并将其值返回给调用它的函数,它将如何完成任何工作?或者就此而言,一段时间之后怎么没有吹掉堆栈呢?即使像Haskell这样的惰性语言也会在某些时候遇到堆栈溢出。
对这些事情的解释将不胜感激,因为它完全让我感到困惑。
答案 0 :(得分:8)
这可以用于简单的情况这一事实不应该是一个惊喜:由于懒惰和垃圾收集,我们已经在Haskell中舒适地使用无限数据结构。只要您的最终结果不依赖于同时拥有所有价值,就可以在您开始时收集或者不首先强制收集它们。
这就是为什么这个经典的Fibonacci示例在常量¹空间中运行:一旦计算出下两个,就不需要列表中的前一个条目,所以只要你没有任何其他指针就可以收集它们。清单。
fib n = fibs !! n
where fibs = 0 : 1 : zipWith (+) fibs (drop 1 fibs)
尝试针对不同的输入运行此功能并查看内存使用情况。 (使用+RTS -s
运行。)
(如果你想用图表更详细的解释,请看看我写的this post。)
关键是,即使程序员可以获得无限量的信息,如果没有别的东西依赖它,我们仍然可以收集大部分信息。
可以使用完全相同的逻辑来有效地实施FRP程序。
当然,一切都不那么容易。在fibs
示例中,如果我们有一个指向fibs
列表开头的活动指针,内存使用率会上升。如果您的计算依赖于过多的过去数据,那么FRP也会发生同样的事情:它被称为时间泄漏。
处理时间泄漏是实施高效,良好行为的FRP框架的一个悬而未决的问题。很难提供富有表现力的FRP抽象,但不允许使用差的甚至是灾难性的内存。我相信大多数当前的方法最终会提供抽象的FRP类型以及一系列有效的操作,这些操作不太可能导致这些类型的泄漏;一种特别极端的形式是Arrowized FRP,它根本不提供行为/信号类型,而是用信号之间的转换表示所有内容(如箭头所示)。
我自己从未尝试过实施一个好的FRP系统,所以我无法更详细地解释这些问题。如果您对此主题的更多详细信息感兴趣,那么Conal Elliott的博客 - this post是一个很好的起点。您还可以查看他撰写的一些论文,如"Push-Pull Functional Reactive Programming"以及有关该主题的其他论文,包括一些关于像"Functional Reactive Programming, Continued"这样的Arrowized FRP(几乎随机选择)。
<强>脚注强>
¹这不是真正的恒定空间,因为中间结果会变得更大。但它应该在内存中保持一定数量的列表单元格。
答案 1 :(得分:1)
关于泄漏部分问题的时间:这确实是实施FRP的主要挑战之一。然而,FRP研究人员和实施者已经找到了几种避免它们的方法。
这完全取决于您为信号提供的精确API。主要问题是您是否提供更高阶的FRP 。这通常采用&#34; monadic join&#34;信号原语:将信号信号转换为信号的方法,或者换言之,用于产生在许多其他信号之间动态切换的信号的API。这样的API非常强大,但可能会引入潜在的时间泄漏,即您提出的问题:需要将所有信号的先前值保留在内存中。然而,正如Heinrich Apfelmus在对先前答案的评论中提到的那样,有一些方法可以通过使用类型系统或其他方式以某种方式限制高阶API来解决这个问题。请参阅该评论以获取进一步解释的链接。
许多FRP库根本不提供高阶API,因此(非常容易)避免时间泄漏问题。在这种情况下,你提到了榆树,正如在{34}下提到的here;信号不是榆树中的单子&#34;。这确实是以表现力为代价的,因为没有提供强大的monadic API,但并不是每个人都认为你需要在FRP框架/库中使用这种API的一般功能。
最后,我推荐Elm的主要作者Evan Czaplicki撰写的an interesting presentation,他很好地解释了这些问题,并提供了解决这些问题的可能方法的概述。他根据FRP方法的解决方法对FRP方法进行了分类。