【问题标题】:Haskell mutable map/treeHaskell 可变映射/树
【发布时间】:2011-01-15 01:26:34
【问题描述】:

我正在寻找 Haskell 中的可变(平衡)树/映射/哈希表或如何在函数中模拟它的方法。 IE。当我多次调用同一个函数时,结构被保留。到目前为止,我已经尝试过 Data.HashTable (还可以,但有点慢)并尝试过 Data.Array.Judy 但我无法使其与 GHC 6.10.4 一起使用。还有其他选择吗?

【问题讨论】:

    标签: data-structures haskell hashtable mutable


    【解决方案1】:

    如果你想要可变状态,你可以拥有它。只需继续传递更新的地图,或将其保存在 state monad 中(结果是相同的)。

    import qualified Data.Map as Map
    import Control.Monad.ST
    import Data.STRef
    memoize :: Ord k => (k -> ST s a) -> ST s (k -> ST s a)
    memoize f = do
        mc <- newSTRef Map.empty
        return $ \k -> do
            c <- readSTRef mc
            case Map.lookup k c of
                Just a -> return a
                Nothing -> do a <- f k
                              writeSTRef mc (Map.insert k a c) >> return a
    

    你可以这样使用它。 (实际上,您可能还想添加一种从缓存中清除项目的方法。)

    import Control.Monad
    main :: IO ()
    main = do
        fib <- stToIO $ fixST $ \fib -> memoize $ \n ->
            if n < 2 then return n else liftM2 (+) (fib (n-1)) (fib (n-2))
        mapM_ (print <=< stToIO . fib) [1..10000]
    

    风险自负,您可以不安全地通过所有需要它的东西来逃避线程状态的要求。

    import System.IO.Unsafe
    unsafeMemoize :: Ord k => (k -> a) -> k -> a
    unsafeMemoize f = unsafePerformIO $ do
        f' <- stToIO $ memoize $ return . f
        return $ unsafePerformIO . stToIO . f'
    
    fib :: Integer -> Integer
    fib = unsafeMemoize $ \n -> if n < 2 then n else fib (n-1) + fib (n-2)
    
    main :: IO ()
    main = mapM_ (print . fib) [1..1000]
    

    【讨论】:

    • 我还是不明白这东西的工作原理:)但我对 IO monad 和 IOref 做了大致相同的事情。令人惊讶的是,它非常快,但并没有计算距离测角函数快:) 至少我学了一些 Haskell。
    【解决方案2】:

    还有其他选择吗?

    对像 Data.Map 这样的纯函数式字典的可变引用。

    【讨论】:

      【解决方案3】:

      如果我没看错您的 cmets,那么您的结构可能需要计算大约 500k 的总值。计算成本很高,因此您希望它们只完成一次,并且在随后的访问中,您只需要不重新计算的值。

      在这种情况下,充分利用 Haskell 的懒惰! ~500k 并不是那么大:只需构建所有答案的地图,然后根据需要获取。第一次获取将强制计算,相同答案的后续获取将重用相同的结果,如果您从未获取特定计算 - 它永远不会发生!

      您可以在文件PointCloud.hs 中找到使用 3D 点距离作为计算的这个想法的一个小实现。该文件使用Debug.Trace 记录计算实际完成的时间:

      > ghc --make PointCloud.hs 
      [1 of 1] Compiling Main             ( PointCloud.hs, PointCloud.o )
      Linking PointCloud ...
      
      > ./PointCloud 
      (1,2)
      (<calc (1,2)>)
      Just 1.0
      (1,2)
      Just 1.0
      (1,5)
      (<calc (1,5)>)
      Just 1.0
      (1,2)
      Just 1.0
      

      【讨论】:

      • 我需要大约 500K 的计算,但是域大约是 30 亿,我不知道我需要哪个 500K。
      • 嗯,也许对大多数系统来说太大了,无法以这种方式攻击它。没有害处,无论如何,为 PointCloud.hs 开发代码很有趣。感谢这个有趣的问题!
      【解决方案4】:

      基于@Ramsey 的回答,我还建议您重新构思您的函数以获取地图并返回修改后的地图。然后使用好的 ol'Data.Map 进行编码,这在修改方面非常有效。这是一个模式:

      import qualified Data.Map as Map
      
      -- | takes input and a map, and returns a result and a modified map
      myFunc :: a -> Map.Map k v -> (r, Map.Map k v)
      myFunc a m = … -- put your function here
      
      -- | run myFunc over a list of inputs, gathering the outputs
      mapFuncWithMap :: [a] -> Map.Map k v -> ([r], Map.Map k v)
      mapFuncWithMap as m0 = foldr step ([], m0) as
          where step a (rs, m) = let (r, m') = myFunc a m in (r:rs, m')
          -- this starts with an initial map, uses successive versions of the map
          -- on each iteration, and returns a tuple of the results, and the final map
      
      -- | run myFunc over a list of inputs, gathering the outputs
      mapFunc :: [a] -> [r]
      mapFunc as = fst $ mapFuncWithMap as Map.empty
          -- same as above, but starts with an empty map, and ignores the final map
      

      很容易抽象出这种模式,并使 mapFuncWithMap 通用于以这种方式使用映射的函数。

      【讨论】:

      • 完美! +1 并注意函数类型Map.Map k v -&gt; (r, Map.Map k v) 等价于状态单子!使用来自Control.Monad.StateMonadState (Map.Map k v) r 类型。
      • 我正在使用一个在树本身上工作的优化函数。我有很多“点”,如果我有一个可变结构,我可以将它附加到每个点 - 这样会更有效率在一棵大树上拥有所有点。生成的地图将有大约 500.000 个点,而附加到每个“点”的单个点将只有大约 100-1000 个。我可能会尝试这种方法,看看它是否能提高速度。
      【解决方案5】:

      虽然您要求使用可变类型,但我建议您使用不可变数据结构,并将连续版本作为参数传递给您的函数。

      关于使用哪种数据结构,

      问题是我不能使用(或者我不知道如何)使用非可变类型。

      如果幸运的话,您可以将表数据结构作为额外参数传递给每个需要它的函数。但是,如果您的表格需要广泛分布,您可能希望使用state monad,其中状态是表格的内容。

      如果您尝试记忆,可以尝试 Conal Elliott 博客中的一些惰性记忆技巧,但是一旦超出整数参数,惰性记忆就会变得非常模糊——我不建议您作为初学者尝试.也许您可以发布有关您要解决的更广泛问题的问题?通常对于 Haskell 和可变性,问题是如何在某种范围内包含突变或更新。

      在没有任何全局可变变量的情况下学习编程并不容易。

      【讨论】:

      • 问题是我不能使用(或者我不知道如何)使用非可变类型。我正在尝试构建一个“缓存”功能,到目前为止,不同的解决方案都非常糟糕。我尝试了此处概述的 HashTable 方法:stackoverflow.com/questions/2217289/… 我不知道如何使用非可变数据结构编写相同的方法。
      • @ondra:我试图在我的答案中添加一些指导方针,但这确实有助于了解更多关于这个问题的信息。我看到了您的另一个问题,使用浮点键进行记忆可能会非常痛苦。如果你能对问题的更大背景多说一些,你可能会得到更多有用的帮助。
      • 我仍在尝试解决相同的问题。我已经修改了这个问题,以便我知道每个作为函数参数的“对象”都有一个唯一的 Int32。这让我可以合理有效地缓存这些东西,但我不确定它是否允许我在 Ints 上使用 memoization。我要解决的问题是:适用于球体的优化问题。我正在计算点之间的距离 - 我可以“懒惰地计算”一些测角操作,但不是全部。只有大约 5% 的计算是唯一的,所以如果我可以缓存点之间的距离,我可以节省很多时间。
      • @ondra:你能负担得起球体的离散化,引入一些量化吗?如果是这样,请编写代码以将整个解决方案作为答案,然后找到一种懒惰地计算它的方法。另外,还有一个问题:您是只做球体的表面还是内部?我有一个模糊的想法,您可以以余弦和几个乘法运算的成本来执行此操作,与在大缓存中追逐指针相比,这是一个巨大的变化......
      • 我只需要一个表面。这可以用一个 cos() 和一个 acos() 来计算(所有其他 4 个必要的 sin/cos 操作都可以延迟计算)。到目前为止,我已经做了很多整数运算来避免这样做。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-07-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-03-21
      相关资源
      最近更新 更多