纯函数是仅依赖于其声明的输入及其内部算法来产生其输出的函数。它不会从“外部世界”读取任何其他值——函数范围之外的世界——而且它不会修改外界的任何值。
以下惰性计算的示例都改变了它们的内部状态,但通常仍被认为是纯函数式的,因为它们总是产生相同的结果并且除了内部状态之外没有任何副作用:
lazy val x = 1
// state 1: x is not computed
x
// state 2: x is 1
val ll = LazyList.continually(0)
// state 1: ll = LazyList(<not computed>)
ll(0)
// state 2: ll = LazyList(0, <not computed>)
在您的情况下,等效项是使用私有的、可变的 Map(如您可能已经找到的实现),例如:
def memoize[A, B](f: A => B): A => B = {
val cache = mutable.Map.empty[A, B]
(a: A) => cache.getOrElseUpdate(a, f(a))
}
请注意,缓存不是公开的。
所以,对于一个纯的函数f,如果不查看内存消耗、时间、反射或其他有害的东西,你将无法从外部判断f是否被调用了两次,或者g是否缓存了f的结果。
从这个意义上说,副作用只是打印输出、写入公共变量、文件等。
因此,这个实现被认为纯的(至少在斯卡拉)。
避免可变集合
如果你真的想要避免 var 和可变集合,您需要更改 memoize 方法的签名。
这是因为如果 g 不能改变内部状态,它在初始化后就不能记忆任何新的东西。
一个(低效但简单)的例子是
def memoizeOneValue[A, B](f: A => B)(a: A): (B, A => B) = {
val b = f(a)
val g = (v: A) => if (v == a) b else f(v)
(b, g)
}
val (b1, g) = memoizeOneValue(f, a1)
val (b2, h) = memoizeOneValue(g, a2)
// ...
f(a1) 的结果将缓存在g 中,但不会缓存其他内容。然后,您可以链接它并始终获得新功能。
如果您对它的更快版本感兴趣,请参阅@esse 的答案,它的作用相同,但效率更高(使用不可变映射,所以 O(log(n)) 而不是上面的函数链接列表,O(n))。