我有一个带参数并产生结果的函数。不幸的是,函数产生结果需要很长时间。使用相同的输入经常调用该函数,这就是为什么我可以方便地缓存结果。像
这样的东西let cachedFunction = createCache slowFunction
in (cachedFunction 3.1) + (cachedFunction 4.2) + (cachedFunction 3.1)
我正在研究Data.Array,虽然数组是懒惰的,但我需要用对列表(使用listArray)初始化它 - 这是不切实际的。如果'密钥'是例如'Double'类型,我根本无法初始化它,即使理论上我可以为每个可能的输入分配一个Integer,我也有几万个可能的输入,我实际上只使用了少数几个。我需要使用函数而不是列表来初始化数组(或者,最好是哈希表,因为只使用少量的resutls)。
更新:我正在阅读备忘录文章,据我所知,MemoTrie可以按我想要的方式工作。也许。有人可以尝试生成'cachedFunction'吗?对于一个需要2个Double参数的慢函数?或者,或者,在〜[0..1亿]的域中采用一个不会占用所有内存的Int参数?
答案 0 :(得分:17)
嗯,有Data.HashTable
。然而,散列表不能很好地使用不可变数据和引用透明度,因此我认为它看起来没有多大用处。
对于少量值,将它们存储在搜索树(例如Data.Map
)中可能足够快。如果你可以忍受对Double
进行一些修改,那么更强大的解决方案就是使用类似trie的结构,例如Data.IntMap
;这些查找时间主要与密钥长度成比例,并且集合大小大致不变。如果Int
限制太多,你可以挖掘Hackage,找到在使用密钥类型方面更灵活的trie库。
至于如何缓存结果,我认为你想要的通常称为"memoization"。如果您想按需计算和记忆结果,该技术的要点是定义一个包含所有可能结果的索引数据结构,这样当您要求特定结果时,它仅强制执行得到你想要的答案所需的计算。常见示例通常涉及索引到列表,但相同的原则应适用于任何非严格的数据结构。根据经验,非函数值(包括无限递归数据结构)通常会被运行时缓存,而不是函数结果,所以诀窍是将所有计算都包含在顶级定义中,而不是取决于任何论点。
编辑: MemoTrie示例啊!
这是一个快速而肮脏的概念证明;可能存在更好的方法。
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
import Data.MemoTrie
import Data.Binary
import Data.ByteString.Lazy hiding (map)
mangle :: Double -> [Int]
mangle = map fromIntegral . unpack . encode
unmangle :: [Int] -> Double
unmangle = decode . pack . map fromIntegral
instance HasTrie Double where
data Double :->: a = DoubleTrie ([Int] :->: a)
trie f = DoubleTrie $ trie $ f . unmangle
untrie (DoubleTrie t) = untrie t . mangle
slow x
| x < 1 = 1
| otherwise = slow (x / 2) + slow (x / 3)
memoSlow :: Double -> Integer
memoSlow = memo slow
请注意MemoTrie包使用的GHC扩展;希望这不是问题。在GHCi中加载它并尝试使用(10 ^ 6)或(10 ^ 7)之类的内容调用slow
与memoSlow
,以便查看它的实际效果。
将此概括为具有多个参数或诸如此类的函数应该相当简单。有关使用MemoTrie的更多详细信息,您可能会发现this blog post by its author有帮助。
答案 1 :(得分:4)
请参阅memoization
答案 2 :(得分:3)
GHC的运行时系统中有许多工具可以明确支持memoization。
不幸的是,memoization并不是一件兼备的事情,因此我们需要支持几种不同的方法来应对不同的用户需求。
您可能会发现1999年的原始文章很有用,因为它包含了几个实现作为示例:
Simon Peyton Jones,Simon Marlow和Conal Elliott的Stretching the Storage Manager: Weak Pointers and Stable Names in Haskell
答案 3 :(得分:2)
您可以将慢速函数编写为高阶函数,返回函数本身。因此,您可以在慢函数内部执行所有预处理,并在返回的(希望快速)函数中执行每个计算中不同的部分。示例可能如下所示: (SML代码,但想法应该清楚)
fun computeComplicatedThing (x:float) (y:float) = (* ... some very complicated computation *)
fun computeComplicatedThingFast = computeComplicatedThing 3.14 (* provide x, do computation that needs only x *)
val result1 = computeComplicatedThingFast 2.71 (* provide y, do computation that needs x and y *)
val result2 = computeComplicatedThingFast 2.81
val result3 = computeComplicatedThingFast 2.91
答案 4 :(得分:2)
我会添加自己的解决方案,这似乎也很慢。第一个参数是一个返回Int32的函数 - 这是参数的唯一标识符。如果您想通过不同的方式(例如通过'id')唯一地识别它,则必须将H.new中的第二个参数更改为不同的哈希函数。我将尝试找出如何使用Data.Map并测试我是否获得更快的结果。
import qualified Data.HashTable as H
import Data.Int
import System.IO.Unsafe
cache :: (a -> Int32) -> (a -> b) -> (a -> b)
cache ident f = unsafePerformIO $ createfunc
where
createfunc = do
storage <- H.new (==) id
return (doit storage)
doit storage = unsafePerformIO . comp
where
comp x = do
look <- H.lookup storage (ident x)
case look of
Just res -> return res
Nothing -> do
result <- return (f x)
H.insert storage (ident x) result
return result
答案 5 :(得分:1)
我有几万种可能的输入,我实际上只使用了少数几种。我需要使用函数而不是列表来初始化数组。
我会选择listArray (start, end) (map func [start..end])
func
上面没有真正被调用过。 Haskell是懒惰的并且创建了thunks,当实际需要值时将对其进行评估。答案 6 :(得分:0)
我不知道具体的haskell,但如何在一些哈希数据结构中保留现有答案(可能被称为字典或hashmap)?您可以将慢速函数包装在另一个首先检查地图的函数中,如果没有找到答案,则只调用慢函数。
您可以通过将地图的大小限制为特定大小来实现它的幻想,当它达到该大小时,抛弃最近最少使用的条目。为此,您还需要保留密钥到时间戳映射的映射。