记忆的纯函数本身是否被视为纯函数?


47

假设fn(x)是一个纯粹的函数,它执行一些昂贵的操作,例如返回的素数列表x

假设我们制作了一个相同功能的记忆版本memoizedFn(x)。对于给定的输入,它始终返回相同的结果,但它会保留先前结果的专用缓存以提高性能。

从形式上来讲,被memoizedFn(x)认为是纯洁的?

还是在FP讨论中有其他名称或限定词来指代此功能?(即,具有副作用的函数可能会影响后续调用的计算复杂性,但可能不会影响返回值。)


24
也许对于纯粹主义者而言,这不是纯粹的,但对于务实的人来说,“足够” ;-)
布朗

2
@DocBrown我同意,只是想知道是否有一个更为正式的术语“足够纯”
卡勒姆

13
执行一个纯函数很可能会修改处理器的指令高速缓存,分支预测器等。但是对于纯粹主义者而言,这也可能是“足够的”-否则您可能会完全忘记纯函数。
gnasher729

10
@callum不,没有“足够纯”的正式定义。在讨论纯度和两个“参照透明”调用的语义等效性时,您始终必须确切说明要应用的语义。在较低的实现细节级别上,它将始终崩溃,并具有不同的内存效果或时序。这就是为什么您必须务实:什么层次的细节对于推理代码很有用?
Bergi

3
然后,为了实用主义,我要说的是,纯度取决于您是否将计算时间视为输出的一部分。funcx(){sleep(cached_time--); return 0;}每次都会返回相同的val,但性能会有所不同
Mars

Answers:


41

是。纯函数的记忆版本也是纯函数。

功能纯净度所关心的只是输入参数对函数返回值的影响(传递相同的输入应始终产生相同的输出)以及与全局状态相关的任何副作用(例如,向终端,UI或网络发送的文本) 。计算时间和额外的内存使用与功能纯度无关。

纯函数的缓存对于程序几乎是不可见的。如果函数式编程语言可以确定这样做会有所帮助,则可以将其自动优化为函数的记忆版本。在实践中,自动确定何时记下记忆是有益的实际上是一个难题,但是这种优化是有效的。


19

Wikipedia将“纯函数”定义为具有以下属性的函数:

  • 对于相同的参数,其返回值是相同的(局部静态变量,非局部变量,可变引用参数或来自I / O设备的输入流无变化)。

  • 它的评估没有副作用(本地静态变量,非本地变量,可变引用参数或I / O流不会发生突变)。

实际上,纯函数会在给定相同输入的情况下返回相同的输出,并且不会影响该函数之外的任何其他内容。 为了纯粹起见,只要给定相同的输入返回相同的输出,该函数如何计算其返回值就无关紧要。

像Haskell这样的功能纯净的语言通常会使用备注来通过缓存先前计算的结果来加快其功能。


16
我可能会错过一些东西,但是如何保持高速缓存而又没有副作用呢?
val

1
通过将其保留在函数中。
罗伯特·哈维

4
“局部静态变量无突变”似乎也排除了调用之间持久存在的局部变量。
val

3
这实际上并不能回答问题,即使您似乎暗示是的,也很纯粹。
火星

6
@val你是正确的:这种情况需要放松一点。他所指的纯功能性备忘录没有任何静态数据的可见变异。发生的结果是,在第一次调用该函数时会计算并记录结果,并在每次调用该函数时返回相同的值。许多语言对此都有一个习惯用法:static constC ++中的局部变量(而不是C)或Haskell中的惰性计算数据结构。您还需要一个条件:初始化必须是线程安全的。
戴维斯洛

7

是的,记忆的纯函数通常称为纯函数。这在Haskell这样的语言中尤其常见,在该语言中,内置的功能是记忆,惰性评估,不可变的结果。

有一个重要的警告:备忘录功能必须是线程安全的,否则当两个线程都尝试调用它时,您可能会遇到竞争状态。

计算机科学家以这种方式使用“纯功能”一词的一个例子是Conal Elliott的有关自动记忆的博客文章

也许令人惊讶的是,可以使用惰性函数语言简单且纯粹地在功能上实现备忘录。

在同行评审的文献中有很多例子,并且已经使用了数十年。例如,1995年的这篇论文“在现实世界的AI系统中使用自动记忆化作为软件工程工具”在第5.2节中使用了非常相似的语言来描述我们今天所说的纯函数:

记忆仅适用于真正的功能,不适用于程序。也就是说,如果函数的结果未完全由其输入参数确定地确定,则使用备注将给出错误的结果。通过鼓励在整个系统中使用功能性编程风格,将增加可成功记忆的功能数量。

一些命令式语言也有类似的习惯用法。例如,static constC ++中的变量在使用其值之前仅被初始化一次,并且永不突变。


3

这取决于您的操作方式。

通常人们想通过变异某种缓存字典来记忆。这具有与不纯突变相关的所有问题,例如,必须担心并发性,担心缓存过大等。

但是,您可以记忆而不会导致不纯正的内存突变。这个答案就是一个例子,在这里我通过一个lengths参数在外部跟踪记忆的值。

Robert Harvey提供的链接中,使用惰性评估来避免副作用。

有时会看到的另一种技术是在IO类型的上下文中显式地将备忘录标记为不纯的副作用,例如使用cats-effect的备忘录功能

最后一个观点指出,有时目标只是封装突变而不是消除突变。大多数函数式程序员认为将杂质明确和封装起来“足够纯净”。

如果您希望一个术语将其与真正的纯函数区分开,我认为只需说“用可变字典记忆”就足够了。这使人们知道如何安全地使用它。


我不认为任何素净解决方案解决了上述问题:当你失去任何并发担心,你也失去了任何机会,就像两个同时开始呼叫collatz(100)collatz(200)合作。和IIUIC一样,缓存增长太大的问题仍然存在(尽管Haskell对此可能有一些不错的技巧?)。
maaartinus

注意:IO是纯净的。和上的所有不纯方法IO都命名为unsafeAsync.memoize也是纯净的,所以我们不必满足于“足够纯净” :)
塞缪尔

2

通常,返回列表的函数根本不是纯函数,因为它需要分配存储空间,并且因此可能会失败(例如,通过抛出非纯异常)。具有值类型并且可以将列表表示为有界大小的值类型的语言可能不会出现此问题。出于这个原因,您的示例可能并不纯净。

通常,如果可以以无故障情况的方式来进行记忆(例如,通过为记忆的结果静态分配存储,并在该语言允许线程的情况下通过内部同步来控制对它们的访问),则可以合理地考虑使用此功能纯。


0

您可以使用状态monad来实现无副作用的备忘录

[State monad]基本上是一个函数S =>(S,A),其中S是代表您的状态的类型,而A是该函数产生的结果-Cats State

在您的情况下,状态将是备注值或什么都不是(即Haskell Maybe或Scala Option[A])。如果存在建议的值,则将其返回为A,否则A计算并返回为过渡状态和结果。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.