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 都被记忆了。