在面向对象的语言中,当我需要在已知的生命周期中缓存/记忆函数的结果时,我通常会遵循这种模式:
这种基于对象的方法与此处描述的基于函数的memoization模式非常相似:http://www.bardiak.com/2012/01/javascript-memoization-pattern.html
这种方法的主要好处是结果只保留缓存对象的生命周期。常见的用例是处理工作项列表。对于每个工作项,一个为该项创建缓存对象,使用该缓存对象处理工作项,然后在继续下一个工作项之前丢弃工作项和缓存对象。
在Haskell中实现短暂的memoization的好方法是什么?问题的答案取决于要缓存的函数是纯粹的还是涉及IO?
重申一下 - 看到涉及IO的功能的解决方案会很好。
答案 0 :(得分:12)
让我们使用Luke Palmer的记忆库:Data.MemoCombinators
import qualified Data.MemoCombinators as Memo
import Data.Function (fix) -- we'll need this too
我要定义的东西与他的库的方式略有不同,但它基本相同(而且兼容)。 “可记忆”的东西将自己作为输入,并产生“真实”的东西。
type Memoizable a = a -> a
“memoizer”接受一个函数并生成它的memoized版本。
type Memoizer a b = (a -> b) -> a -> b
让我们写一点函数将这两件事放在一起。给定Memoizable
函数和Memoizer
,我们需要生成的memoized函数。
runMemo :: Memoizer a b -> Memoizable (a -> b) -> a -> b
runMemo memo f = fix (f . memo)
使用fixpoint combinator(fix
)这有点神奇。不要管那个;如果你有兴趣,你可以谷歌。
所以让我们写一个经典的fib例子的Memoizable
版本:
fib :: Memoizable (Integer -> Integer)
fib self = go
where go 0 = 1
go 1 = 1
go n = self (n-1) + self (n-2)
使用self
约定使代码简单明了。请记住,self
是我们期望的这个函数的memoized版本,因此递归调用应该在self
上。现在点起ghci。
ghci> let fib' = runMemo Memo.integral fib
ghci> fib' 10000
WALL OF NUMBERS CRANKED OUT RIDICULOUSLY FAST
现在,关于runMemo
的一件很酷的事情是,您可以创建多个相同功能的新记忆版本,并且它们不共享内存库。这意味着我可以编写一个本地创建并使用fib'
的函数,但是只要fib'
超出范围(或更早,取决于编译器的智能),它可以垃圾收集。 不必在顶层记忆。这可能会或可能不会与依赖unsafePerformIO
的记忆技术很好地配合。 Data.MemoCombinators
使用纯粹的,懒惰的Trie,与runMemo
完美契合。您可以根据需要简单地创建memoized函数,而不是创建一个本质上成为memoization manager的对象。问题是如果你的函数是递归的,那么它必须写成Memoizable
。好消息是你可以插入你想要的任何Memoizer
。你甚至可以使用:
noMemo :: Memoizer a b
noMemo f = f
ghci> let fib' = runMemo noMemo fib
ghci> fib' 30 -- wait a while; it's computing stupidly
1346269
答案 1 :(得分:4)
Lazy-Haskell编程在某种程度上是一种极端的记忆范式。此外,无论你在命令式语言中做什么都可以在Haskell中使用IO monad,ST monad,monad变换器,箭头,或者你的名字。
唯一的问题是这些抽象设备比你提到的命令式等效设备要复杂得多,而且它们需要一个非常深刻的思维 - 重新布线。
答案 2 :(得分:2)
我相信上述答案都比必要的复杂得多,尽管它们可能比我要描述的更容易携带。
据我了解,ghc
中有一条规则,即当输入包含lambda表达式时,每个值只计算一次。因此,您可以按如下方式创建完全短暂的memoization对象。
import qualified Data.Vector as V
indexerVector :: (t -> Int) -> V.Vector t -> Int -> [t]
indexerVector idx vec = \e -> tbl ! e
where m = maximum $ map idx $ V.toList vec
tbl = V.accumulate (flip (:)) (V.replicate m [])
(V.map (\v -> (idx v, v)) vec)
这是做什么的?它根据第一个参数Data.Vector t
计算的整数将vec
中传递的所有元素作为第二个参数idx
分组,并将其分组保留为Data.Vector [t]
。它返回类型为Int -> [t]
的函数,该函数通过此预先计算的索引值查找此分组。
我们的编译器ghc
已承诺tbl
只有在我们调用indexerVector
时才会被淹没一次。因此,我们可能会将\e -> tbl ! e
返回的lambda表达式indexVector
分配给另一个值,我们可能会反复使用这些值,而不必担心tbl
会被重新计算。您可以在trace
上插入tbl
来验证这一点。
简而言之,您的缓存对象就是这个lambda表达式。
我发现通过返回像这样的lambda表达式,几乎可以用短期对象完成的任何事情都可以完成。
答案 3 :(得分:1)
你也可以在haskell中使用相同的模式。延迟评估将负责检查是否已经评估了值。已经提到过多次,但代码示例可能很有用。在下面的示例中,memoedValue
只会在需要时计算一次。
data Memoed = Memoed
{ value :: Int
, memoedValue :: Int
}
memo :: Int -> Memoed
memo i = Memoed
{ value = i
, memoedValue = expensiveComputation i
}
更好的是,你可以记住依赖于其他记忆值的值。你应该避免依赖循环。他们可以导致不确定
data Memoed = Memoed
{ value :: Int
, memoedValue1 :: Int
, memoedValue2 :: Int
}
memo :: Int -> Memoed
memo i = r
where
r = Memoed
{ value = i
, memoedValue1 = expensiveComputation i
, memoedValue2 = anotherComputation (memoedValue1 r)
}