我一直在寻找Data.MemoCombinators的来源,但我无法真正看到它的核心位置。
请向我解释所有这些组合器背后的逻辑以及机制它们如何在实际编程中加速您的程序。
我正在寻找此实现的细节,并可选择与其他Haskell方法进行比较/对比来进行记忆。我理解什么是memoization,并且不正在寻找它的工作原理的描述。
答案 0 :(得分:58)
该库是众所周知的记忆技术的直接组合。让我们从规范的例子开始:
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
获取一个函数并返回相同函数的memoized版本。任何在语义上都具有此类型且具有此类型的函数称为“Int
的memoizer”(偶数id
,它不会记忆)。我们推广到这个抽象:
type Memo a = forall r. (a -> r) -> (a -> r)
因此Memo a
是a
的记事本,它从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之外发生的每个值的情况一样)。所以我们只是映射到一对和后面。关键点在于我们通过枚举域来提升对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等。但是使用它是一种痛苦,并且它还对要记忆的函数施加严格限制(例如,没有无限的左递归)。我不相信有任何库实现这种技术。
这回答了你的好奇吗?如果没有,或许明确指出你感到困惑的几点?
答案 1 :(得分:18)
心脏是bits
功能:
-- | Memoize an ordered type with a bits instance.
bits :: (Ord a, Bits a) => Memo a
bits f = IntTrie.apply (fmap f IntTrie.identity)
它是唯一可以为您提供unit :: Memo ()
值的函数(除了普通的Memo a
)。它使用了与此page中关于Haskell memoization的相同的想法。第2节显示了使用列表的最简单的memoization策略,第3节使用类似于memocombinators中使用的IntTree
的自然二叉树来执行相同的操作。
基本思想是使用类似(map fib [0 ..] !!)
的构造或memocombinators案例中的构造 - IntTrie.apply (fmap f IntTrie.identity)
。这里要注意的是IntTie.apply
和!!
以及IntTrie.identity
和[0..]
之间的对应关系。
下一步是使用其他类型的参数记忆函数。这是通过wrap
函数完成的,该函数使用类型a
和b
之间的同构来构造Memo b
中的Memo a
。例如:
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和memoizing多个参数等类型。
答案 2 :(得分:7)
部分工作由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 -> b
获取一个函数,并将其映射到由 a
的所有可能值索引的数据结构中。包含b
的值。这样的数据结构应该以两种方式延迟 - 首先它应该是它所持有的值的延迟。其次,应该懒得自己制作。前者默认使用非严格语言。后者是通过使用广义尝试来完成的。
memocombinators,memotrie等的各种方法都只是创建各种类型数据结构的尝试组合的方法,以便为日益复杂的结构简单构建尝试。
答案 3 :(得分:0)
@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 时不会重新计算。我不清楚这些记忆组合器是否具有相同的行为(即顶级记忆而不是仅仅禁止在“计算”内部重新计算),如果是,为什么呢?