FRP在内存方面是如何处理的?
Posted
技术标签:
【中文标题】FRP在内存方面是如何处理的?【英文标题】:How is FRP handled in terms of memory? 【发布时间】:2015-02-26 02:06:19 【问题描述】:阅读 FRP(Functional Reactive Programming)与标准命令式方法相比,我对它的直观性和逻辑性感到惊讶;然而有一件事让我感到困惑.. 计算机如何不会立即耗尽内存呢?
根据我从 [here] 收集到的信息,在 FRP 中,值更改的完整历史(过去、现在和未来)是一流时间>。这个概念立即在我脑海中敲响了警钟,说如果它在没有立即从内存中清除过去值的环境中使用,它必须非常快速地消耗你的内存。
阅读 [Fran] 时,我注意到几个示例具有没有终止条件的递归定义函数。如果函数永远不会终止并将其值返回给调用它的函数,它怎么会完成任何事情呢?或者就此而言,它如何在一段时间后不炸毁堆栈?即使是像 Haskell 这样的惰性语言也会在某些时候遇到堆栈溢出。
非常感谢对这些事情的解释,因为它完全让我感到困惑。
【问题讨论】:
【参考方案1】:关于时间泄漏部分您的问题:这确实是实施 FRP 的主要挑战之一。然而,FRP 研究人员和实施者已经找到了几种避免它们的方法。
这完全取决于您为信号提供的精确 API。主要问题是您是否提供高阶FRP。这通常采用信号的“monadic join”原语的形式:一种将信号的信号转换为信号的方法,或者换句话说,一种 API 来产生在多个其他信号之间动态切换的信号。这样的 API 非常强大,但可能会引入时间泄漏的可能性,即您提出的问题:需要将所有信号的先前值保存在内存中。但是,正如 Heinrich Apfelmus 在对先前答案的评论中提到的那样,有一些方法可以通过使用类型系统或其他方式以某种方式限制高阶 API 来解决这个问题。请参阅该评论以获取进一步解释的链接。
许多 FRP 库根本不提供高阶 API,因此(很容易)避免了时间泄漏问题。您提到了 Elm,在这种情况下,正如 here 在“信号不是 Elm 中的单子”下提到的。这确实是以表现力为代价的,因为没有提供强大的一元 API,但并不是每个人都认为您需要 FRP 框架/库中这种 API 的通用功能。
最后,我推荐 Elm 的主要作者 Evan Czaplicki 的 an interesting presentation,他很好地解释了这些问题并概述了解决这些问题的可能方法。他根据 FRP 方法的解决方式对其进行分类。
【讨论】:
【参考方案2】:这对于简单的情况下可以工作这一事实并不令人惊讶:由于惰性和垃圾收集,我们已经在 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 类型以及一组不太可能导致此类泄漏的操作;一种特别极端的形式是 Arrowized FRP,它根本不提供行为/信号类型,而是通过信号之间的转换(如箭头)来表达一切。
我从来没有尝试过自己实现一个好的 FRP 系统,所以我无法更详细地解释这些问题。如果您对有关此主题的更多详细信息感兴趣,可以查看 Conal Elliott 的博客 — this post 是一个很好的起点。您还可以查看他撰写的一些论文,例如 "Push-Pull Functional Reactive Programming" 以及有关该主题的其他论文,包括一些关于箭头化 FRP 的论文,例如 "Functional Reactive Programming, Continued"(几乎是随机选择的)。
脚注
¹它不是真正恒定空间,因为中间结果本身会变大。但它应该在内存中保持恒定数量的列表单元格。
【讨论】:
但是,如果完整的历史我是一等公民,那么一定有办法以某种方式回到起点,对吧?就像在围绕 FRP 构建的语言 Elm 中一样,它甚至有一个“时间旅行调试器”,允许您随意浏览程序的时间线。这一定会在某个地方造成时间泄漏,不是吗? 关键是,如果您从一开始就确实需要数据,它只会导致泄漏——而且这种泄漏确实会发生。但是,如果您的程序以不使用所有数据的方式编写,则可以安全地进行垃圾回收。 我确信 Elm 的调试器必须在内存中存储很多,只是因为无法绕过它。但是一个普通的 Elm 程序可以收集其中的大部分,给你合理的内存使用。 @ElectricCoffee:这些函数之所以有效,是因为它们的返回值始终是一个惰性构造函数:一种捆绑代码的数据结构,说明如何计算这些值的领域。从更底层的角度来看,为此类递归函数生成的目标代码不会调用自身;相反,它返回一个结构体,该结构体有一个指向函数本身的指针。返回结构的使用者,通过访问字段或避免这样做,选择递归调用是否以及何时实际发生。 我想提一下,这几年时间泄漏问题已经解决了。 older blog post 描述了问题和可能的解决方案。 more recent post 描述了最近版本的响应式香蕉中的解决方案。以上是关于FRP在内存方面是如何处理的?的主要内容,如果未能解决你的问题,请参考以下文章
__eq__ 在 Python 中是如何处理的以及按啥顺序处理?