有没有一种纯函数式的方法来完成这个任务?
没有。就使用给定的签名和最狭义的纯函数而言,没有。
简而言之:使用可变集合即可!
g
的不纯性
val g = memoize(f)
g(a)
你会期望调用
g(a)
会发生什么?
如果
g(a)
记忆了结果,则(内部)状态必须更改,这样在调用
g(a)
后状态与之前不同。由于这可以从外部观察到,因此对
g
的调用具有副作用,这使得您的程序是不纯的。
从你提到的书籍中,2.5节是关于"纯函数和非纯函数":
> [...] 函数应该满足以下两个条件:
>
> - 给定相同的输入,总是产生相同的输出
> - **没有副作用**
>
> 被称为纯函数。
这真的算是一个副作用吗?
通常情况下,至少在Scala中,
内部状态的更改
不被认为是副作用。
查看
Scala Book中的定义:
> 纯函数是一种只依赖于其声明的输入和其内部算法来生成输出的函数。它不会从“函数范围之外”的任何其他值中读取数据,并且不会修改外部世界中的任何值。
以下这些懒计算的例子都更改了它们的内部状态,但通常仍被认为是纯函数,因为它们总是产生相同的结果,并且除了内部状态之外没有副作用:
lazy val x = 1
x
val ll = LazyList.continually(0)
ll(0)
在你的情况下,等价的做法是使用一个私有的可变 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的结果。
在这个意义上,副作用只指像输出打印、写入公共变量、文件等等这些东西。
因此,至少在Scala中,这个实现被认为是纯的。
避免可变集合
如果您真的想避免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)
< p >
f(a1)
的结果将被缓存在
g
中,但没有其他的。然后,您可以链接此并始终获得一个新函数。
如果您对更快的版本感兴趣,请参见@esse的答案,它执行相同的操作,但更有效率(使用不可变映射,因此为O (log(n))
而不是上面的函数链表,O(n)
)。