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

Posted

技术标签:

【中文标题】函数式编程语言中的自动记忆【英文标题】:Automatic memoizing in functional programming languages 【发布时间】:2011-08-10 13:55:07 【问题描述】:

我一直认为 Haskell 会进行某种自动智能记忆。例如,朴素的斐波那契实现

fib 0 = 0
fib 1 = 1
fib n = fib (n-2) + fib (n-1)

因此会很快。现在我读了this,看来我错了——Haskell 似乎没有自动记忆。还是我理解错了?

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

实现memoization的常用方法有哪些?在我见过的所有示例实现中,它们都使用哈希图,但它的大小没有任何限制。显然,这在实践中是行不通的,因为您需要某种限制。鉴于此,它变得更加复杂,因为当您达到极限时,您必须丢弃一些数据。那里变得复杂了:限制是否应该是动态的并且经常使用的函数应该比不经常使用的函数有更高的限制?当你达到极限时你会扔掉什么条目?只是最近用过的吗?在这种情况下,您还需要另外对数据进行排序。您可以使用链表和哈希映射的某种组合来实现这一点。这是常见的方式吗?

您能否链接(或参考)一些常见的实际实现?

谢谢, 阿尔伯特


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


编辑:可以在here 找到一些自己的想法以及示例实现(有限制)。


编辑:我不是在尝试解决特定应用程序中的特定问题。我正在寻找可以全局应用于(纯函数式)程序的所有函数的记忆化通用解决方案(因此不实现内存限制的算法不是解决方案)。当然,(可能)没有最佳/最佳解决方案。但这让我的问题不那么有趣了。

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

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

【问题讨论】:

如果每个函数的每个调用都被记忆,空间使用会爆炸。 Haskell 实际上所做的是在共享这些值时不重新计算已经评估过的东西。 碰巧,最近有一篇关于这个确切主题的博客文章:augustss.blogspot.com/2011/04/… @Don:这正是我在问题中所说的。 @MatrixFrog:似乎它并没有真正解决我在问题中描述的问题,即主要是内存限制及其解决方案/实现。 听起来你真正感兴趣的问题是垃圾收集。 【参考方案1】:

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

x = somethingVeryLong

和你拥有的相同范围内的其他地方

y = f x
z = g x

那么 x 将只计算一次。

This package 展示了如何使用各种键和查找表来存储记忆值。记忆通常在一个更大的函数的单个调用中使用,因此记忆的值不会永远存在(正如您所说的那样,这将是一个问题)。如果您想要一个也使用 LRU 或其他东西忘记旧值的记忆器,那么我认为您可能需要将它放在状态单子或其他东西中;使用传统的记忆方法,你不能让 Haskell 表现得像那样。

【讨论】:

您链接的软件包似乎也忽略了有关内存限制的问题。你知道任何不会忽略这一点的代码(无论是什么语言)吗?【参考方案2】:

没有确切的答案,但此页面:http://www.haskell.org/haskellwiki/Memoization 提供了有关 Haskell 中记忆化的想法,还展示了可能感兴趣的斐波那契数列的基于列表的实现。

【讨论】:

【参考方案3】:

我在评论中说您的要求听起来像是垃圾收集。我这么认为是因为您有兴趣管理有限的内存池,不时清除它,以免它溢出。

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

但是,记忆化通常不是通过限制保留的结果来完成的;上述算法所需的突变通常是不成熟的。不过,不要让这让你灰心。你有一些有趣的想法,这些想法可能对迄今为止在 Haskell 中探索记忆可能性的有价值的补充。

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

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

【讨论】:

页面替换算法的链接很有趣。其中许多确实可以应用于记忆。其中 memoization 还是比较特别的。【参考方案4】:

Haskell 似乎没有自动记忆。还是我理解错了?

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

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

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

实现memoization的常用方法有哪些?

没有真正的common 方式来实现记忆。对于类似 fib 的模式(只有一个参数,它是一个数字),在 fib-example 中使用的记忆可以设计一个通用解决方案(哈希图之一)并且它会起作用,但它也可能不是您的特定问题的最佳选择.

使用 memoisation 你有副作用,所以你可能希望缓存存在于 State monad 中。然而,一般来说,你希望你的算法尽可能地纯净,所以如果它有递归,你已经陷入了一些混乱。这是因为您将递归调用函数的记忆版本,但这需要在 State monad 中运行,所以现在您的整个函数必须在 State monad 中运行。这也会影响懒惰,但你可以试试lazy state monad。

记住这一点:自动记忆是很难实现的。但你可以轻松地走很长一段路。自动记忆函数可能涉及程序转换,其中在固定点编写东西可能会有很长的路要走。

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

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

【讨论】:

纯记忆没有副作用;它只是将表示参数或参数集的 thunk 逐步转换为具体值。然而,Albert 正在寻找更像缓存的东西,其中旧值被遗忘。这确实需要某种副作用,即使从调用者的角度来看,该函数仍然是纯函数。 您链接的 LRU C++ 示例很有趣。这终于是一个真正的解决方案——我在这个线程中读到的第一个解决方案。由于有序映射,它并不是最优的——哈希映射在这里更合适。然后你已经完全按照我自己提供的解决方案结束了(请参阅我的问题中的最后一个链接)。所以我仍然想念一些替代/更好的解决方案,这些解决方案也解决了其他一些问题(比如动态更改限制等)。 顺便说一句,“好的自动记忆很难”,这是为什么呢?这意味着人们已经尝试过,但表现不佳。我还没有看到这样的基准。你能链接到任何一个吗? @Paul Johnson:在您更新内存并将项目添加到记忆表的意义上的副作用。这也可能会影响您的程序,也许您的程序会因为记忆化而占用大量内存而中断。 @Albert,当您想要进行自动记忆时,您需要考虑很多事情。您允许记忆哪些功能?你会记住作为函数的参数吗?如果您只想使用 Haskell 进行记忆,则需要使用类似 fib(安全)的模式,或者对于更复杂的类型,使用哈希映射。但是你不能搭载 Haskell 的 thunk 评估,你需要自己管理地图。据我现在所见,这需要将所有内容都提升到 State Monad 或使用 unsafeperform【参考方案5】:

例如实现自动记忆,你可以看看Factor programming language和它的Memoization vocabulary。例如,简单的斐波那契数生成器:

: 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 输入和相应的输出将透明地存储在内存字典中。

Factor 语言语法可能会令人困惑 :)。我建议您观看video presentation from Google Tech Talks 的有关 Factor 以了解,如何以这种方式实现 memoization。

【讨论】:

【参考方案6】:

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

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

【讨论】:

以上是关于函数式编程语言中的自动记忆的主要内容,如果未能解决你的问题,请参考以下文章

函数式编程--为什么要学习函数式编程?

C#中的函数式编程

Java经典类库-Guava中的函数式编程讲解

[HMLY]11.iOS函数式编程的实现&&响应式编程概念

函数式编程中的函数—函数式编程的多态

深入浅出-iOS函数式编程的实现 && 响应式编程概念