Warning: file_get_contents(/data/phpspider/zhask/data//catemap/6/haskell/8.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Haskell函数定义和缓存数组_Haskell_Memoization - Fatal编程技术网

Haskell函数定义和缓存数组

Haskell函数定义和缓存数组,haskell,memoization,Haskell,Memoization,我有一个关于在Haskell中使用数组实现缓存(记忆化)的问题。以下模式有效: f = (fA !) where fA = listArray... 但事实并非如此(程序的速度表明每次调用或其他操作都会重新创建阵列): 在where子句之外(在“全局范围”中)定义fA也适用于这两种模式 我希望有人能给我提供一个技术上的解释,说明上述两种模式之间的区别 请注意,我使用的是最新的GHC,我不确定这是编译器的特性还是语言本身的一部分 编辑:!用于阵列访问,所以fA!5表示C++语法中的FA(5)

我有一个关于在Haskell中使用数组实现缓存(记忆化)的问题。以下模式有效:

f = (fA !)
  where fA = listArray...
但事实并非如此(程序的速度表明每次调用或其他操作都会重新创建阵列):

在where子句之外(在“全局范围”中)定义fA也适用于这两种模式

我希望有人能给我提供一个技术上的解释,说明上述两种模式之间的区别

请注意,我使用的是最新的GHC,我不确定这是编译器的特性还是语言本身的一部分


编辑:!用于阵列访问,所以fA!5表示C++语法中的FA(5)。我对Haskell的理解是,(fA!)n将与(fA!n)相同……而且对我来说,写“fn=fA!n”(不带括号)更为传统。无论如何,无论我如何插入括号,我都会得到相同的行为。

找到发生了什么的最好方法是告诉编译器使用
-v4
输出其中间表示形式。输出量很大,读起来有点困难,但应该允许您确切地了解生成代码中的差异,以及编译器是如何到达的

您可能会注意到,在第一个示例中,
fA
正在被移出函数(到“全局范围”)。在第二个示例中,它可能不是(这意味着每次调用都会重新创建它)

它没有被移出函数的一个可能原因是编译器认为它取决于
n
的值。在您的工作示例中,
n
对于
fA
来说是不可依赖的

但是,我认为编译器在第二个示例中避免将
fA
移到外部的原因是,它试图避免空间泄漏。考虑一下,如果<代码> FA<代码>,而不是数组,则是一个无限列表(在上面使用了<代码>!> /代码>运算符)。想象一下,您曾经用一个大数字(例如
F1000
)来调用它,后来只用小数字来调用它(
F2
F3
F12
)。先前调用中的10000个元素仍在内存中,浪费了空间。因此,为了避免这种情况,每次调用函数时,编译器都会再次创建
fA

避免空间泄漏可能不会发生在第一个示例中,因为在这种情况下,
f
实际上只调用一次,返回一个闭包(我们现在处于纯函数和命令世界的前沿,所以事情变得更微妙)。这个闭包替换了原来的函数,它将永远不会被调用,因此
fA
只被调用一次(因此优化器可以自由地将它移到函数之外)。在第二个示例中,
f
不会被闭包替换(因为它的值取决于参数),因此会再次被调用

如果您想进一步了解这一点(这将有助于阅读
-v4
输出),您可以看看这篇论文()


至于你的最后一个问题,我认为这是编译器的特点(但我可能错了)。但是,如果所有编译器都做相同的事情,即使它不是语言的一部分,我也不会感到惊讶。

Haskell标准没有规定行为上的差异。它所要说的是,函数是相同的(在相同的输入下会产生相同的输出)

然而,在这种情况下,有一种简单的方法可以预测大多数编译器所遵循的时间和内存性能。我再次强调,这不是必要的,只是大多数编译器都这样做

首先,将两个示例改写为纯lambda表达式,展开部分:

f = let fA = listArray ... in \n -> fA ! n
f' = \n -> let fA = listArray ... in fA ! n
编译器使用let绑定来表示共享。保证是在给定的环境中(一组局部变量,lambda body,类似的东西),不带参数的let绑定的右侧最多只计算一次。前者的fA环境是整个程序,因为它不在任何lambda下,但后者的环境较小,因为它在lambda下

这意味着,在后一种情况下,可以针对每个不同的n对fA进行一次评估,而在前一种情况下,这是禁止的

即使使用多参数函数,我们也可以看到这种模式的效果:

g x y = (a ! y) where a = [ x ^ y' | y' <- [0..] ]
g' x = (\y -> a ! y) where a = [ x ^ y' | y' <- [0..] ]
我们可能会多次计算2^100,但是:

let k = g' 2 in k 100 + k 100
我们只计算一次


如果您正在使用Memorization,我推荐data memo Combinators on Hackage,这是一个不同形状的备忘录表库,因此您不必自己滚动。

很酷,感谢您的回答,这对我很有帮助,我一定会查看data memo Combinators on Hackage。我来自C++背景,一直在努力理解Haskell在给定程序中会做什么(主要是在复杂性方面),而教程似乎没有涉及到这一点。

这里发布了一个类似的问题:-虽然说得更清楚一些,并且有一些很好的回答。
let k = g 2 in k 100 + k 100
let k = g' 2 in k 100 + k 100