Haskell缓存函数的结果

时间:2010-02-07 15:44:07

标签: caching haskell memoization

我有一个带参数并产生结果的函数。不幸的是,函数产生结果需要很长时间。使用相同的输入经常调用该函数,这就是为什么我可以方便地缓存结果。像

这样的东西
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参数?

7 个答案:

答案 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)之类的内容调用slowmemoSlow,以便查看它的实际效果。

将此概括为具有多个参数或诸如此类的函数应该相当简单。有关使用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,当实际需要值时将对其进行评估。
  • 使用普通数组时,您始终需要初始化其值。因此无论如何,创建这些thunk所需的工作是必要的。
  • 数万人远不是很多。如果你有数万亿,那么我建议使用哈希表yada yada

答案 6 :(得分:0)

我不知道具体的haskell,但如何在一些哈希数据结构中保留现有答案(可能被称为字典或hashmap)?您可以将慢速函数包装在另一个首先检查地图的函数中,如果没有找到答案,则只调用慢函数。

您可以通过将地图的大小限制为特定大小来实现它的幻想,当它达到该大小时,抛弃最近最少使用的条目。为此,您还需要保留密钥到时间戳映射的映射。