GHC测试套件中的这种简短的记忆功能如何工作?

时间:2018-02-05 11:56:23

标签: haskell memoization

Here是以下memoization函数的完整可运行代码:

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

我试图通过手动扩展这些术语来解决这个问题,但是这种扩展看起来就像是缓慢的,未经修饰的功能。它是如何工作的?注释掉的代码是如何派生出来的?

1 个答案:

答案 0 :(得分:7)

解释起来非常棘手。我将从一个更简单的例子开始。

必须记住

之间的区别
\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

两者都计算相同的功能。但是,当使用参数f 0调用时,前者将始终(重新)计算0。相反,后者仅在第一次使用参数f 0调用时计算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 ->的左侧显示=,否则我们会失去记忆!

等效地:

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的备忘录,而不是!

因此,我们可以递归

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),这将为fz案例分配另一个1。 这看起来不错,但是fz不会持续很长时间,因为每次调用g' x时它都会被重新分配,从而否定了memoization。

要解决此问题,我们会使用另一个变量,因此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最多只评估一次,并会为fz案例分配另一个1。此fz现在不会消失。

递归地说,人们可以确信现在所有的值f n都被记忆了。