GHC 测试套件中的这个简短的记忆功能是如何工作的?
How does this short memoization function in the GHC test suite work?
Here 是以下记忆函数的完整可运行代码:
memo f = g
where
fz = f Z
fs = memo (f . S)
g Z = fz
g (S n) = fs n
-- It is a BAD BUG to inline 'fs' inside g
-- and that happened in 6.4.1, resulting in exponential behaviour
-- memo f = g (f Z) (memo (f . S))
-- = g (f Z) (g (f (S Z)) (memo (f . S . S)))
-- = g (f Z) (g (f (S Z)) (g (f (S (S Z))) (memo (f . S . S . S))))
fib' :: Nat -> Integer
fib' = memo fib
where
fib Z = 0
fib (S Z) = 1
fib (S (S n)) = fib' (S n) + fib' n
我试图通过手动扩展项来解决这个问题,但这种扩展看起来就像是缓慢的、未记忆的函数。它是如何工作的?以及注释掉的代码是如何导出的?
这很难解释。我将从一个更简单的示例开始。
必须牢记
之间的区别
\x -> let fz = f 0 in if x==0 then fz else f x
let fz = f 0 in \x -> if x==0 then fz else f x
两者计算相同的函数。但是,当使用参数 0
调用时,前者将始终(重新)计算 f 0
。相反,后者只会在第一次使用参数 0
调用时计算 f 0
-- 当发生这种情况时, fz
被评估,并且结果永远存储在那里,以便它可以下次需要 fz
时再次使用。
这与
没有太大区别
f 0 + f 0
let fz = f 0 in fz + fz
后者只会调用 f 0
一次,因为第二次 fz
已经被评估了。
因此,我们可以实现 f
的简单记忆,仅存储 f 0
,如下所示:
g = let fz = f 0 in \x -> if x==0 then fz else f x
等价于:
g = \x -> if x==0 then fz else f x
where
fz = f 0
注意这里不能把\x ->
放在=
的左边,否则会丢失memoization!
等价于:
g = g'
where
fz = f 0
g' = \x -> if x==0 then fz else f x
现在我们可以毫无问题地把\x ->
放在左边。
等价于:
g = g'
where
fz = f 0
g' x = if x==0 then fz else f x
等价于:
g = g'
where
fz = f 0
g' 0 = fz
g' x = f x
现在,这只会记忆 f 0
而不是每个 f n
。实际上,计算 g 4
两次将导致 f 4
计算两次。
为了避免这种情况,我们可以开始让 g
处理任何函数 f
而不是固定函数:
g f = g' -- f is now a parameter
where
fz = f 0
g' 0 = fz
g' x = f x
现在,我们利用它:
-- for any f, x
g f x = f x
-- hence, in particular
g (f . succ) (pred x) = (f . succ) (pred x) = f (succ (pred x)) = f x
所以,g (f . succ) (pred x)
是一种复杂的写法f x
。像往常一样,g
将函数记忆为零。然而这是(f . succ) 0 = f 1
。这样,我们在1
获得了memoization,而不是!
因此,我们可以递归
g f = g' -- f is now a parameter
where
fz = f 0
g' 0 = fz
g' x = g (f . succ) (pred x)
如果用 0
调用,这将使用 fz
存储 f 0
,记忆它。
如果用 1
调用,这将调用 g (f . succ)
,这将为 1
情况分配另一个 fz
。
这看起来不错,但是 fz
不会持续很长时间,因为每次调用 g' x
时都会重新分配它,从而否定记忆。
为了解决这个问题,我们使用了另一个变量,这样 g (f . succ)
最多只计算一次。
g f = g' -- f is now a parameter
where
fz = f 0
fs = g (f . succ)
g' 0 = fz
g' x = fs (pred x)
这里,fs
最多被评估一次,并且会导致为1
情况分配另一个fz
。这个fz
现在不会消失了。
递归地,可以确信现在所有值 f n
都被记忆了。
Here 是以下记忆函数的完整可运行代码:
memo f = g
where
fz = f Z
fs = memo (f . S)
g Z = fz
g (S n) = fs n
-- It is a BAD BUG to inline 'fs' inside g
-- and that happened in 6.4.1, resulting in exponential behaviour
-- memo f = g (f Z) (memo (f . S))
-- = g (f Z) (g (f (S Z)) (memo (f . S . S)))
-- = g (f Z) (g (f (S Z)) (g (f (S (S Z))) (memo (f . S . S . S))))
fib' :: Nat -> Integer
fib' = memo fib
where
fib Z = 0
fib (S Z) = 1
fib (S (S n)) = fib' (S n) + fib' n
我试图通过手动扩展项来解决这个问题,但这种扩展看起来就像是缓慢的、未记忆的函数。它是如何工作的?以及注释掉的代码是如何导出的?
这很难解释。我将从一个更简单的示例开始。
必须牢记
之间的区别\x -> let fz = f 0 in if x==0 then fz else f x
let fz = f 0 in \x -> if x==0 then fz else f x
两者计算相同的函数。但是,当使用参数 0
调用时,前者将始终(重新)计算 f 0
。相反,后者只会在第一次使用参数 0
调用时计算 f 0
-- 当发生这种情况时, fz
被评估,并且结果永远存储在那里,以便它可以下次需要 fz
时再次使用。
这与
没有太大区别f 0 + f 0
let fz = f 0 in fz + fz
后者只会调用 f 0
一次,因为第二次 fz
已经被评估了。
因此,我们可以实现 f
的简单记忆,仅存储 f 0
,如下所示:
g = let fz = f 0 in \x -> if x==0 then fz else f x
等价于:
g = \x -> if x==0 then fz else f x
where
fz = f 0
注意这里不能把\x ->
放在=
的左边,否则会丢失memoization!
等价于:
g = g'
where
fz = f 0
g' = \x -> if x==0 then fz else f x
现在我们可以毫无问题地把\x ->
放在左边。
等价于:
g = g'
where
fz = f 0
g' x = if x==0 then fz else f x
等价于:
g = g'
where
fz = f 0
g' 0 = fz
g' x = f x
现在,这只会记忆 f 0
而不是每个 f n
。实际上,计算 g 4
两次将导致 f 4
计算两次。
为了避免这种情况,我们可以开始让 g
处理任何函数 f
而不是固定函数:
g f = g' -- f is now a parameter
where
fz = f 0
g' 0 = fz
g' x = f x
现在,我们利用它:
-- for any f, x
g f x = f x
-- hence, in particular
g (f . succ) (pred x) = (f . succ) (pred x) = f (succ (pred x)) = f x
所以,g (f . succ) (pred x)
是一种复杂的写法f x
。像往常一样,g
将函数记忆为零。然而这是(f . succ) 0 = f 1
。这样,我们在1
获得了memoization,而不是!
因此,我们可以递归
g f = g' -- f is now a parameter
where
fz = f 0
g' 0 = fz
g' x = g (f . succ) (pred x)
如果用 0
调用,这将使用 fz
存储 f 0
,记忆它。
如果用 1
调用,这将调用 g (f . succ)
,这将为 1
情况分配另一个 fz
。
这看起来不错,但是 fz
不会持续很长时间,因为每次调用 g' x
时都会重新分配它,从而否定记忆。
为了解决这个问题,我们使用了另一个变量,这样 g (f . succ)
最多只计算一次。
g f = g' -- f is now a parameter
where
fz = f 0
fs = g (f . succ)
g' 0 = fz
g' x = fs (pred x)
这里,fs
最多被评估一次,并且会导致为1
情况分配另一个fz
。这个fz
现在不会消失了。
递归地,可以确信现在所有值 f n
都被记忆了。