Haskell GHC测试套件中的这种简短记忆功能是如何工作的?
是以下记忆功能的完整可运行代码:Haskell GHC测试套件中的这种简短记忆功能是如何工作的?,haskell,memoization,Haskell,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) (mem
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
调用前者时,前者将始终(重新)计算f0
。相反,后者将仅在第一次使用参数调用时计算f0
——当这种情况发生时,计算fz
,并将结果永久存储在那里,以便下次需要fz
时可以再次使用
这和我的想法没有太大区别
f 0 + f 0
let fz = f 0 in fz + fz
其中后者将只调用一次f0
,因为第二次fz
将被计算
因此,我们可以实现f
的轻度记忆,只存储f0
,如下所示:
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
g = g'
where
fz = f 0
g' = \x -> if x==0 then fz else f 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
请注意,在这里我们不能将\x->
放在=
的左侧,否则我们将失去记忆功能
相当于:
g = \x -> if x==0 then fz else f x
where
fz = f 0
g = g'
where
fz = f 0
g' = \x -> if x==0 then fz else f 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
现在我们可以毫无问题地将\x->
带到左侧
相当于:
g = \x -> if x==0 then fz else f x
where
fz = f 0
g = g'
where
fz = f 0
g' = \x -> if x==0 then fz else f 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
相当于:
g = \x -> if x==0 then fz else f x
where
fz = f 0
g = g'
where
fz = f 0
g' = \x -> if x==0 then fz else f 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
现在,它只记忆f0
,而不是每个fn
。实际上,两次计算g4
将导致f4
计算两次
为了避免这种情况,我们可以开始使用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)
是一种复杂的书写方式。与往常一样,g
将函数记忆为零。但是这是(f.succ)0=f1
。通过这种方式,我们在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
存储f0
,并将其存储
如果使用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
现在不会消失
递归地,我们可以确信,现在所有的值
fn
都被记录下来了。有点整洁g
是一个闭包,包含f Z
和memo(f.S)
的thunk,这本身就是不同f
的g
版本。因此,您将得到一个列表形状的闭包链,每个闭包都包含一个thunk forfn
。我认为您可以忽略注释掉的代码。它似乎与给定的memo
的定义不匹配,因为如果您尝试键入g(f Z)
,您会发现它具有typeNat
,因此无法应用于参数(memo(f.S))
。可能它指的是一个较旧版本的备忘录
,也可能作者只是“大声思考”并留下了评论。非常有趣的是,看到这么一个简短、简单的函数花了多少心思!与其他一些记忆函数不同,这个函数确实也适用于正常整数,如示例中所示,而不仅仅适用于惰性NAT等。