问题 FRP如何处理内存?


阅读FRP(功能反应式编程我很惊讶它看起来有多直观和合乎逻辑 标准 势在必行;然而有一件事让我感到困惑..计算机怎么不立即耗尽内存呢?

从我收集到的[这里],就是在FRP中 完成 价值变化的历史(过去,现在和未来)是 头等舱。这个想法立刻在我脑海里响起一声警告说它已经吃掉了你的记忆 非常 如果在没有立即从内存中清除值的过去的环境中使用它,则快速。

阅读有关 [弗兰],我注意到几个例子已经递归地定义了没有终止条件的函数。如果函数永远不会终止并将其值返回给调用它的函数,它将如何完成任何工作?或者就此而言,一段时间之后怎么没有吹掉堆栈呢?即使是像Haskell这样的惰性语言也会在某些时候遇到堆栈溢出。

对这些事情的解释将不胜感激,因为它完全让我感到困惑。


10437
2017-12-29 21:41


起源



答案:


事实上,这可以用于简单的情况不应该是一个惊喜:由于懒惰和垃圾收集,我们已经在Haskell中舒适地使用无限数据结构。只要您的最终结果不依赖于同时拥有所有值,就可以在您开始时收集它们,或者在第一时间不强制它们。

这就是为什么这个经典的Fibonacci示例在常量¹空间中运行的原因:一旦计算出下两个,就不需要列表中的前一个条目,因此只要你没有任何其他指向列表的指针就会收集它们。

fib n = fibs !! n
  where fibs = 0 : 1 : zipWith (+) fibs (drop 1 fibs)

尝试针对不同的输入运行此函数并查看内存使用情况。 (运行它 +RTS -s。)

(如果您想用图表进行更详细的解释,请查看 这个帖子 我写了。)

关键是,即使程序员可以获得无限量的信息,如果没有别的东西依赖它,我们仍然可以垃圾收集大部分信息。

完全相同的逻辑可用于有效地实施FRP程序。

当然,一切都不那么容易。在里面 fibs 例如,如果我们有一个指向开头的活动指针,内存使用率会上升 fibs 名单。如果您的计算依赖于过多的过去数据,那么FRP也会发生同样的事情:它被称为a 时间泄漏

处理时间泄漏是实施高效,良好行为的FRP框架的开放性问题之一。很难提供富有表现力的FRP抽象,但不允许使用差的甚至是灾难性的内存。我相信大多数当前的方法最终会提供抽象的FRP类型以及一组不太可能导致这类泄漏的有福操作。一种特别极端的形式是Arrowized FRP,它根本不提供行为/信号类型,而是表达所有内容 转换 信号之间(如箭头所示)。

我自己从未尝试过实施一个好的FRP系统,所以我无法更详细地解释这些问题。如果你对这个主题的更多细节感兴趣,那么一个值得一看的好地方就是Conal Elliott的博客 这个帖子 作为一个很好的起点。你也可以看看他写的一些论文 “推挽功能反应编程” 以及关于这个主题的其他论文,包括一些关于Arrowized FRP的文章 “功能反应式编程,续” (几乎随机选择)。

脚注

¹不是  恒定的空间,因为中间结果自己变大。但它 应该 在内存中保持一定数量的列表单元格。


8
2017-12-30 02:54



但如果完整的历史我是一流的公民,那么必定有办法以某种方式回到起点,对吧?就像在围绕FRP构建的语言Elm一样,它甚至还有一个“时间旅行调试器”,允许您随意浏览程序的时间线。这必须在某处造成时间泄漏,不是吗? - Electric Coffee
关键在于它实际上只会导致泄漏 需要 从一开始就一直存在数据 - 那些泄漏确实发生了。但是,如果您的程序是以不使用所有数据的方式编写的,则可以安全地进行垃圾回收。 - Tikhon Jelvis
我确信Elm的调试器必须存储 很多 在记忆中,仅仅因为没有办法绕过它。但正常的Elm程序可以收集大部分内容,为您提供合理的内存使用量。 - Tikhon Jelvis
@ElectricCoffee:这些函数有效,因为它们的返回值始终是a 懒惰的构造函数:捆绑的数据结构 码 说如何计算字段的值。从更低级别的角度来看,为这种递归函数生成的目标代码不会调用自身;相反,它返回一个结构,该结构具有指向函数本身的指针。返回结构的使用者通过访问字段或避免这样做,选择是否以及何时实际发生递归调用。 - Luis Casillas
我想提一下近年来时间泄漏问题已经解决了。一个 较旧的博文 描述了问题和可能的解决方案。一个 最近的帖子 描述了最近版本的反应性香蕉的解决方案。 - Heinrich Apfelmus


答案:


事实上,这可以用于简单的情况不应该是一个惊喜:由于懒惰和垃圾收集,我们已经在Haskell中舒适地使用无限数据结构。只要您的最终结果不依赖于同时拥有所有值,就可以在您开始时收集它们,或者在第一时间不强制它们。

这就是为什么这个经典的Fibonacci示例在常量¹空间中运行的原因:一旦计算出下两个,就不需要列表中的前一个条目,因此只要你没有任何其他指向列表的指针就会收集它们。

fib n = fibs !! n
  where fibs = 0 : 1 : zipWith (+) fibs (drop 1 fibs)

尝试针对不同的输入运行此函数并查看内存使用情况。 (运行它 +RTS -s。)

(如果您想用图表进行更详细的解释,请查看 这个帖子 我写了。)

关键是,即使程序员可以获得无限量的信息,如果没有别的东西依赖它,我们仍然可以垃圾收集大部分信息。

完全相同的逻辑可用于有效地实施FRP程序。

当然,一切都不那么容易。在里面 fibs 例如,如果我们有一个指向开头的活动指针,内存使用率会上升 fibs 名单。如果您的计算依赖于过多的过去数据,那么FRP也会发生同样的事情:它被称为a 时间泄漏

处理时间泄漏是实施高效,良好行为的FRP框架的开放性问题之一。很难提供富有表现力的FRP抽象,但不允许使用差的甚至是灾难性的内存。我相信大多数当前的方法最终会提供抽象的FRP类型以及一组不太可能导致这类泄漏的有福操作。一种特别极端的形式是Arrowized FRP,它根本不提供行为/信号类型,而是表达所有内容 转换 信号之间(如箭头所示)。

我自己从未尝试过实施一个好的FRP系统,所以我无法更详细地解释这些问题。如果你对这个主题的更多细节感兴趣,那么一个值得一看的好地方就是Conal Elliott的博客 这个帖子 作为一个很好的起点。你也可以看看他写的一些论文 “推挽功能反应编程” 以及关于这个主题的其他论文,包括一些关于Arrowized FRP的文章 “功能反应式编程,续” (几乎随机选择)。

脚注

¹不是  恒定的空间,因为中间结果自己变大。但它 应该 在内存中保持一定数量的列表单元格。


8
2017-12-30 02:54



但如果完整的历史我是一流的公民,那么必定有办法以某种方式回到起点,对吧?就像在围绕FRP构建的语言Elm一样,它甚至还有一个“时间旅行调试器”,允许您随意浏览程序的时间线。这必须在某处造成时间泄漏,不是吗? - Electric Coffee
关键在于它实际上只会导致泄漏 需要 从一开始就一直存在数据 - 那些泄漏确实发生了。但是,如果您的程序是以不使用所有数据的方式编写的,则可以安全地进行垃圾回收。 - Tikhon Jelvis
我确信Elm的调试器必须存储 很多 在记忆中,仅仅因为没有办法绕过它。但正常的Elm程序可以收集大部分内容,为您提供合理的内存使用量。 - Tikhon Jelvis
@ElectricCoffee:这些函数有效,因为它们的返回值始终是a 懒惰的构造函数:捆绑的数据结构 码 说如何计算字段的值。从更低级别的角度来看,为这种递归函数生成的目标代码不会调用自身;相反,它返回一个结构,该结构具有指向函数本身的指针。返回结构的使用者通过访问字段或避免这样做,选择是否以及何时实际发生递归调用。 - Luis Casillas
我想提一下近年来时间泄漏问题已经解决了。一个 较旧的博文 描述了问题和可能的解决方案。一个 最近的帖子 描述了最近版本的反应性香蕉的解决方案。 - Heinrich Apfelmus


关于泄漏部分问题的时间:这确实是实施FRP的主要挑战之一。然而,FRP研究人员和实施者已经找到了几种避免它们的方法。

这完全取决于您为信号提供的精确API。主要问题是您是否提供 更高阶的FRP。这通常采用信号的“monadic join”原语的形式:将信号信号转换为信号的方式,或者换句话说,API用于产生在许多其他信号之间动态切换的信号。这样的API非常强大,但可以引入潜力 时间泄漏,即您询问的问题:需要将所有信号的先前值保留在内存中。然而,正如Heinrich Apfelmus在对先前答案的评论中提到的那样,有一些方法可以通过使用类型系统或其他方式以某种方式限制高阶API来解决这个问题。请参阅该评论以获取进一步解释的链接。

许多FRP库根本不提供更高阶的API,因此(非常容易)避免了时间泄漏的问题。你提到过榆树,就像这种情况一样 这里 在“信号不是榆树的单子”中。这确实是以表现力为代价的,因为没有提供强大的monadic API,但并不是每个人都认为你需要在FRP框架/库中使用这种API的一般功能。

最后,我建议 一个有趣的演讲 作者:Elm的主要作者Evan Czaplicki,他在解释这些问题方面做得非常出色,并提供了解决这些问题的可能方法的概述。他根据FRP方法的解决方法对FRP方法进行了分类。


1
2018-01-02 12:34