Data.MemoCombinators 是如何工作的?

Posted

技术标签:

【中文标题】Data.MemoCombinators 是如何工作的?【英文标题】:How does Data.MemoCombinators work? 【发布时间】:2011-06-26 04:35:28 【问题描述】:

我一直在寻找Data.MemoCombinators 的来源,但我看不出它的核心在哪里。

请向我解释所有这些组合器背后的逻辑以及它们在现实世界编程中如何实际工作以加速您的程序的机制

我正在寻找 this 实现的细节,并可选择与其他 Haskell 记忆化方法进行比较/对比。我了解什么是记忆化,并且我不是在寻找关于它的一般工作原理的描述。

【问题讨论】:

【参考方案1】:

@luqui 我不清楚的一件事:这是否具有与以下相同的操作行为:

fib :: [Int]
fib = map fib' [0..]
    where fib' 0 = 0
             fib' 1 = 1
             fib' n = fib!!(n-1) + fib!!(n-2)

上面应该记住 fib 在顶层,因此如果你定义了两个函数:

f n = fib!!n + fib!!(n+1)

如果我们再计算f 5,我们得到fib 5在计算fib 6时没有被重新计算。我不清楚 memoization 组合器是否具有相同的行为(即*** memoization 而不是仅禁止在 fib 计算“内部”重新计算),如果是,为什么?

【讨论】:

您的语法错误。 f n = (fib !! n) + (fib !! (n + 1)) -- 你的 fib 不是一个函数,它是一个惰性列表。 luqui's 包装了索引操作。这意味着他的顶层是一个函数,而不是一个恒定的应用形式,因此不会在调用之间被记忆。然而,这并不是方法的缺陷,只是教学上的简化。 好吧,我修正了那个小错误。我无意暗示这是一个缺陷,我只是想知道它是否相同。特别是因为reddit thread 上的某个人建议 fib 一直处于应用形式(您似乎对此矛盾?)。 @sclv,这是不正确的。顶部fib 中的列表,实际上也是 fib 的组合记忆中的数据结构,低于零 lambdas,因此它将在顶层和调用之间被记忆。尝试在实现中将 fib 更改为 fib' 以使其变慢并查看其行为!【参考方案2】:

这个库是众所周知的记忆技术的直接组合。让我们从典型的例子开始:

fib = (map fib' [0..] !!)
    where
    fib' 0 = 0
    fib' 1 = 1
    fib' n = fib (n-1) + fib (n-2)

我将您所说的解释为您知道它的工作原理和原因。所以我将专注于组合化。

我们基本上是在尝试捕捉和概括(map f [0..] !!) 的想法。这个函数的类型是(Int -> r) -> (Int -> r),这很有意义:它从Int -> r 中获取一个函数并返回相同函数的记忆版本。任何在语义上是身份并具有这种类型的函数都称为“Int 的记忆器”(甚至 id,它不记忆)。我们推广到这个抽象:

type Memo a = forall r. (a -> r) -> (a -> r)

因此,Memo aa 的记忆器,从 a 获取一个函数到任何东西,并返回一个已被记忆(或未记忆)的语义相同的函数。

不同记忆器的想法是找到一种方法来枚举具有数据结构的域,将函数映射到它们上,然后索引数据结构。 bool 就是一个很好的例子:

bool :: Memo Bool
bool f = table (f True, f False)
    where
    table (t,f) True = t
    table (t,f) False = f

Bool 中的函数等价于对,除了一对只会对每个组件求值一次(就像出现在 lambda 之外的每个值的情况一样)。所以我们只是映射到一对并返回。要点是我们通过枚举域来提升函数对参数(这里是table的最后一个参数)的评估。

记忆Maybe a 是一个类似的故事,除了现在我们需要知道如何记忆a 以处理Just 的情况。所以 Maybe 的 memoizer 将 a 的 memoizer 作为参数:

maybe :: Memo a -> Memo (Maybe a)
maybe ma f = table (f Nothing, ma (f . Just))
    where
    table (n,j) Nothing = n
    table (n,j) (Just x) = j x

库的其余部分只是这个主题的变体。

它记忆整数类型的方式使用了比[0..] 更合适的结构。这有点复杂,但基本上只是创建了一个无限树(以二进制表示数字以阐明结构):

1
  10
    100
      1000
      1001
    101
      1010
      1011
  11
    110
      1100
      1101
    111
      1110
      1111

因此,在树中查找数字的运行时间与其表示中的位数成正比。

正如 sclv 所指出的,Conal 的 MemoTrie 库使用相同的底层技术,但使用类型类表示而不是组合表示。我们同时独立发布了我们的库(实际上,在几个小时内!)。 Conal 更容易在简单的情况下使用(只有一个函数,memo,它会根据类型确定要使用的备忘录结构),而我的更灵活,你可以这样做:

boundedMemo :: Integer -> Memo Integer
boundedMemo bound f = \z -> if z < bound then memof z else f z
   where
   memof = integral f

仅记忆小于给定界限的值,这是实现项目欧拉问题之一所需的。

还有其他方法,例如在 monad 上公开一个开放的定点函数:

memo :: MonadState ... m => ((Integer -> m r) -> (Integer -> m r)) -> m (Integer -> m r)

这允许更大的灵活性,例如。清除缓存、LRU 等。但是使用起来很麻烦,而且它对要记忆的函数施加了严格的限制(例如,没有无限的左递归)。我不相信有任何库可以实现这种技术。

这是否回答了您的好奇?如果不是,也许把你的困惑点说清楚?

【讨论】:

请问什么!!做?以前没见过。 @Kirakun: !!运算符索引列表中的位置。 [0..] !! 5 == 5.【参考方案3】:

心脏是bits函数:

-- | Memoize an ordered type with a bits instance.
bits :: (Ord a, Bits a) => Memo a
bits f = IntTrie.apply (fmap f IntTrie.identity)

它是唯一可以为您提供Memo a 值的函数(除了琐碎的unit :: Memo ())。它使用与 page 关于 Haskell 记忆的相同想法。第 2 节展示了使用列表的最简单的记忆策略,第 3 节使用类似于 memocombinators 中使用的IntTree 的自然二叉树来做同样的事情。

基本思想是使用像(map fib [0 ..] !!) 这样的结构,或者在memocombinators 的情况下——IntTrie.apply (fmap f IntTrie.identity)。这里要注意的是IntTie.apply!!之间的对应关系,还有IntTrie.identity[0..]之间的对应关系。

下一步是使用其他类型的参数来记忆函数。这是通过wrap 函数完成的,该函数使用ab 类型之间的同构从Memo a 构造Memo b。例如:

Memo.integral f
=>
wrap fromInteger toInteger bits f
=>
bits (f . fromInteger) . toInteger
=>
IntTrie.apply (fmap (f . fromInteger) IntTrie.identity) . toInteger
~> (semantically equivalent)
(map (f . fromInteger) [0..] !!) . toInteger

源代码的其余部分处理 List、Maybe、Either 等类型以及记忆多个参数。

【讨论】:

这个答案并没有得到太多支持,但实际上信息量很大,对 luqui 的回答是一个很好的赞美。 @Dan:我期待这会发生,毕竟他是包的作者 :)【参考方案4】:

部分工作由 IntTrie 完成:http://hackage.haskell.org/package/data-inttrie-0.0.4

Luke 的库是 Conal 的 MemoTrie 库的一个变体,他在此描述:http://conal.net/blog/posts/elegant-memoization-with-functional-memo-tries/

一些进一步的扩展——函数式记忆背后的一般概念是从a -&gt; b获取一个函数,并将其映射到由a所有可能值索引并包含@值的数据结构中987654325@。这样的数据结构应该在两个方面是惰性的——首先它应该在它所持有的值上是惰性的。二是自己懒惰生产。前者默认使用非严格语言。后者是通过使用泛化尝试来完成的。

memocombinators、memotrie 等的各种方法都只是在单个数据结构类型上创建尝试片段组合的方法,以允许为越来越复杂的结构简单构造尝试。

【讨论】:

+1 这里的链接非常棒,可以进一步了解血淋淋的细节(实际上它们很明智,并不是很血腥,你只需要学习一些新词汇词)。

以上是关于Data.MemoCombinators 是如何工作的?的主要内容,如果未能解决你的问题,请参考以下文章

SAP系统中 生产订单如何批量确认?

AsyncTask 如何将一个进程工作到另一个进程?

智慧巡检管理是如何做到的?

软工第一次作业

Dynamics 365Online 如何在21v中提交工单

Discuz常见小问题-如何设置为人工审核