在函数式编程语言中自动记忆

时间:2011-04-21 19:47:01

标签: haskell functional-programming memoization

我一直认为Haskell会做某种自动智能记忆。例如,天真的斐波纳契实现

fib 0 = 0
fib 1 = 1
fib n = fib (n-2) + fib (n-1)
因此,

会很快。现在我读了this,看起来我错了--Haskell似乎没有做自动记忆。或者我是否理解错误?

是否有其他语言进行自动(即隐式,非显式)记忆?

实施备忘录的常用方法有哪些?在我看到的所有示例实现中,它们都使用了散列映射,但其大小没有任何限制。显然,这在实践中不起作用,因为你需要某种限制。鉴于此,它变得更加复杂,因为当你达到极限时,你必须扔掉一些数据。并且它变得复杂:如果限制可能是动态的并且经常使用的功能应该比不常使用的功能具有更高的限制吗?当你达到极限时,你扔掉了什么条目?只是最新使用的一个?在这种情况下,您还需要对数据进行排序。您可以使用链接列表和哈希映射的某种组合来实现这一点。这是常见的方式吗?

你可以链接(或参考)一些常见的实际实现吗?

谢谢, 阿尔伯特


编辑:我最感兴趣的是我所描述的问题,即如何实现这样的限制。对任何解决这个问题的论文的任何引用都会非常好。


编辑:可以找到一些关于示例实现(有限制)的想法here


编辑:我不是要解决特定应用程序中的特定问题。我正在寻找用于memoization的通用解决方案,它可以全局应用于(纯功能)程序的所有功能(因此不实现内存限制的算法不是解决方案)。当然,(可能)没有最佳/最佳解决方案。但这使我的问题不那么有趣。

为了尝试这样的解决方案,我考虑将其添加到Haskell作为优化。我真的很想知道这会有多好。

我想知道是否有人已经这样做了。

6 个答案:

答案 0 :(得分:8)

我在评论中说,你的要求听起来像垃圾收集。我之所以这么想,是因为你有兴趣管理一个有限的内存池,偶尔清除它,以便它不会过去。

现在我考虑一下,它更像是一个虚拟内存page replacement algorithm。您可以阅读维基百科页面,了解用于解决此类问题的各种方法,例如“最近未使用”,“老化”,“时钟”,“第二次机会”等。

然而,通常不会通过限制保留的结果来进行记忆;上述算法所需的突变通常是不合时宜的。但是,不要让那些气馁。你有一些有趣的想法,可以成为探索Haskell中记忆可能性的有价值的补充。

有时,特定的记忆问题很适合有限的记忆。例如,对齐两个基因序列可以使用动态编程(参见维基百科的Dynamic Programming # Sequence alignment)和二维记忆表来完成。但由于给定单元的DP解决方案仅取决于前一行的结果,因此您可以从底部开始并丢弃距离当前行大于1的行。斐波那契数字是相同的:你只需要序列中的前两个数字来计算下一个数字。如果您感兴趣的是 n th 号码,您可以提前放弃任何内容。

大多数记忆都是为了加速存在共享子问题的递归算法。许多此类问题有一种简单的方法对评估进行排序,以便丢弃您不再需要的结果。那时,您只是猜测,使用启发式(如使用频率)来确定谁获得了对有限资源的访问权限。

答案 1 :(得分:6)

不,Haskell不会自动记忆功能。它的作用是存储值,所以如果你有

x = somethingVeryLong

和你在同一范围内的其他地方

y = f x
z = g x

然后x只会被计算一次。

This package显示了如何使用各种键和查找表存储记忆值。记忆通常在较大函数的单个调用中使用,因此记忆值不会永远挂起(正如您所说的那样是一个问题)。如果你想要一个也使用LRU忘记旧值的备忘录,那么我认为你可能需要把它放在一个状态monad或者什么东西;你不能让Haskell使用传统的memoising方法表现得那样。

答案 2 :(得分:6)

  

Haskell似乎没有做自动记忆。或者我是否理解错误?

不,Haskell没有。但是,共享表达式只计算一次。在Paul Johnson给出的示例中,x作为thunk存储在堆上。 yz都可以引用x,因为x在范围内,并且它们引用相同的位置。一旦x被评估,它将仅被评估一次,并且仅保留评估结果。所以这不是真正的记忆,而是实施的结果。

  

是否有其他语言进行自动(即隐式,非显式)记忆?

我已经看到装饰器@memoized出现在一些python源代码中。你当然可以完全为它创建自己的装饰器/实现。完成LRU和您想要使用的其他政策。

  

实施备忘录的常用方法是什么?

没有真正的common方式来实现备忘录。对于类似于fib的模式(只有一个参数,这是一个数字),在fib-example中使用的memoisation可以设计一个通用解决方案(hashmaps one)并且它可以工作,但它对于你的特定问题也可能不是最理想的

通过记忆你会产生副作用,所以你可能希望缓存存在于State monad中。但是,一般来说,您希望尽可能保持算法的纯度,因此如果它有递归,那么您已经陷入了一些混乱。这是因为你将以递归方式调用函数的memoised版本,但是需要在State monad中运行,所以现在你的整个函数必须在State monad中运行。这也会影响懒惰,但您可以尝试lazy state monad

牢记这一点:良好自动记忆很难实现。但是你可以很容易地走很长的路。自动记忆功能可能涉及程序转换,在修复点写东西可能会有很长的路要走。

  

编辑:我最感兴趣的是我所描述的问题,即如何实现这样的限制。对任何解决这个问题的论文的任何引用都会非常好。

一旦掌握了记忆的基本机制,你就可以为你的记忆表调整查找和存储函数,以实现LRU或其他一些保持内存消耗较小的机制。也许你可以从this C++ example获得LRU的想法。

答案 3 :(得分:4)

例如,如果要实现自动记忆,您可以查看Factor programming language及其Memoization vocabulary。例如,简单的Fibonacci数字生成器:

: fib ( n -- n )
    dup 1 > [
        [ 1 - fib ]
        [ 2 - fib ]
        bi +
    ] when ;

可以通过将“:”替换为“MEMO:”

来记忆
MEMO: fib ( n -- n )
    dup 1 > [
        [ 1 - fib ]
        [ 2 - fib ]
        bi +
    ] when ;

在这种情况下,fib输入和相应的输出将透明地存储在内存中的字典中。

因子语言语法可能令人困惑:)。我建议您观看video presentation from Google Tech Talks有关要理解的因素,如何以这种方式实施备忘录。

答案 4 :(得分:1)

没有确切答案,但是这个页面:http://www.haskell.org/haskellwiki/Memoization提供了关于Haskell中的Memoization的想法,并且还显示了可能感兴趣的Fibonacci序列的基于列表的实现。

答案 5 :(得分:0)

在Maple中,您可以使用选项remember

F := proc(n) option remember;
    if n<2 then n else F(n-1)+F(n-2)
    end if
end proc;