假设某人制作了下棋或解决数独游戏的程序。在这种程序中,有一个表示游戏状态的树结构是有意义的。
这棵树非常大,“实际上是无限的”。由于Haskell支持无限数据结构,因此本身不是问题。
一个熟悉的无限数据结构示例:
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
节点仅在首次使用时分配,因此列表占用有限的内存。如果他们不保留对其头部的引用,也可以迭代无限列表,允许垃圾收集器收集其不再需要的部分。
回到树示例 - 假设一个人在树上进行一些迭代,如果仍然需要树的根,则迭代的树节点可能不会被释放(例如,在迭代加深搜索中,树将被迭代)多次,所以需要保留根。)
我想到的这个问题的一个可能的解决方案是使用“unmemo-monad”。
我将尝试使用monadic列表演示这个monad应该做什么:
import Control.Monad.ListT (ListT) -- cabal install List
import Data.Copointed -- cabal install pointed
import Data.List.Class
import Prelude hiding (enumFromTo)
nums :: ListT Unmemo Int -- What is Unmemo?
nums = enumFromTo 0 1000000
main = print $ div (copoint (foldlL (+) 0 nums)) (copoint (lengthL nums))
使用nums :: [Int]
时,nums
在lengthL nums
上进行迭代时,foldlL (+) 0 nums
需要对Unmemo
的引用占用大量内存。
((->) ())
的目的是使运行时不要让节点迭代过来。
我尝试将Unmemo
用作nums :: [Int]
,但它会产生与+RTS -s
相同的结果 - 程序使用大量内存,这可以通过{{1}运行来实现}。
无论如何都要实现我想要的Unmemo
吗?
答案 0 :(得分:4)
与流相同的技巧 - 不直接捕获余数,而是捕获一个值和一个产生余数的函数。您可以根据需要在此基础上添加memoization。
data UTree a = Leaf a | Branch a (a -> [UTree a])
我现在没有心情准确地弄明白,但是这种结构出现了,我敢肯定,自然是一个相当简单的仿函数的共同点。
修改强>
或者这可能更容易理解:http://hackage.haskell.org/packages/archive/streams/0.7.2/doc/html/Data-Stream-Branching.html
在任何一种情况下,诀窍在于您的f
可以选择data N s a = N (s -> (s,[a]))
来表示适当的s
(s是您的状态参数的类型 - - 你展开的种子,如果你愿意的话)。这可能不完全正确,但接近应该做的事情......
但是当然对于实际工作,你可以废弃所有这些,只需像上面那样直接编写数据类型。
修改2
以下代码说明了这可以防止共享。请注意,即使在没有共享的版本中,配置文件中也有驼峰,表示sum和length调用未在恒定空间中运行。我想我们需要一个明确的严格积累来击倒它们。
{-# LANGUAGE DeriveFunctor #-}
import Data.Stream.Branching(Stream(..))
import qualified Data.Stream.Branching as S
import Control.Arrow
import Control.Applicative
import Data.List
data UM s a = UM (s -> Maybe a) deriving Functor
type UStream s a = Stream (UM s) a
runUM s (UM f) = f s
liftUM x = UM $ const (Just x)
nullUM = UM $ const Nothing
buildUStream :: Int -> Int -> Stream (UM ()) Int
buildUStream start end = S.unfold (\x -> (x, go x)) start
where go x
| x < end = liftUM (x + 1)
| otherwise = nullUM
sumUS :: Stream (UM ()) Int -> Int
sumUS x = S.head $ S.scanr (\x us -> maybe 0 id (runUM () us) + x) x
lengthUS :: Stream (UM ()) Int -> Int
lengthUS x = S.head $ S.scanr (\x us -> maybe 0 id (runUM () us) + 1) x
sumUS' :: Stream (UM ()) Int -> Int
sumUS' x = last $ usToList $ liftUM $ S.scanl (+) 0 x
lengthUS' :: Stream (UM ()) Int -> Int
lengthUS' x = last $ usToList $ liftUM $ S.scanl (\acc _ -> acc + 1) 0 x
usToList x = unfoldr (\um -> (S.head &&& S.tail) <$> runUM () um) x
maxNum = 1000000
nums = buildUStream 0 maxNum
numsL :: [Int]
numsL = [0..maxNum]
-- All these need to be run with increased stack to avoid an overflow.
-- This generates an hp file with two humps (i.e. the list is not shared)
main = print $ div (fromIntegral $ sumUS' nums) (fromIntegral $ lengthUS' nums)
-- This generates an hp file as above, and uses somewhat less memory, at the cost of
-- an increased number of GCs. -H helps a lot with that.
-- main = print $ div (fromIntegral $ sumUS nums) (fromIntegral $ lengthUS nums)
-- This generates an hp file with one hump (i.e. the list is shared)
-- main = print $ div (fromIntegral $ sum $ numsL) (fromIntegral $ length $ numsL)