我正在尝试使用Haskell将一个简单(但相当大)的Tree结构保存到二进制文件中。结构看起来像这样:
-- For simplicity assume each Node has only 4 childs data Tree = Node [Tree] | Leaf [Int]以下是我需要在磁盘上查看数据的方式:
对我来说,Haskellers在编写二进制文件时的第一选择是Data.Binary.Put库。但有了这个,我在子弹#1中遇到了问题。特别是,当我要将一个Node写入文件时,为了记下子偏移,我需要知道我当前的偏移量和每个孩子的大小。
这不是Data.Binary.Put所提供的,所以我认为这必须是Monad变换器的完美应用。但即使它看起来很酷且功能齐全,到目前为止我还没有成功使用这种方法。
我问了另外两个我认为可以帮助我解决问题的问题here和here。我必须说,每次我收到非常好的答案,帮助我进一步发展,但不幸的是,我仍然无法解决整个问题。
Here是我到目前为止所做的,它仍然会泄漏太多内存而不实用。
我很想拥有使用这种功能方法的解决方案,但也会对任何其他解决方案表示感谢。
答案 0 :(得分:2)
我会考虑两种基本方法。如果整个序列化结构很容易适合内存,你可以将每个节点序列化为一个惰性字节串,只需使用每个节点的长度来计算当前位置的偏移量。
serializeTree (Leaf nums) = runPut (mapM_ putInt32 nums)
serializeTree (Node subtrees) = mconcat $ header : childBs
where
childBs = map serializeTree subtrees
offsets = scanl (\acc bs -> acc+L.length bs) (fromIntegral $ 2*length subtrees) childBs
header = runPut (mapM_ putInt32 $ init offsets)
另一个选项是,在序列化节点之后,返回并使用适当的数据重写偏移字段。如果树很大,这可能是唯一的选择,但我不知道支持这个的序列化库。这将涉及在IO
和seek
工作到正确的位置。
答案 1 :(得分:2)
我认为你想要的是一个明确的双通解决方案。第一个将您的树转换为带注释大小的树。这个传球迫使树,但事实上,通过打结可以完成没有任何monadic机械。第二次传递是在普通的Put monad中,并且考虑到已经计算了大小注释,应该非常简单。
答案 2 :(得分:2)
这是 sclv 提出的两遍解决方案的实现。
import qualified Data.ByteString.Lazy as L
import Data.Binary.Put
import Data.Word
import Data.List (foldl')
data Tree = Node [Tree] | Leaf [Word32] deriving Show
makeTree 0 = Leaf $ replicate 100 0xdeadbeef
makeTree n = Node $ replicate 4 $ makeTree $ n-1
SizeTree模仿原始树,它不包含数据,但在每个节点上它存储树中相应子节点的大小。
我们需要在内存中使用SizeTree,因此值得使其更紧凑(例如用Uboxed单词替换Ints)。
data SizeTree
= SNode {sz :: Int, chld :: [SizeTree]}
| SLeaf {sz :: Int}
deriving Show
在内存中使用SizeTree可以以流方式序列化原始树。
putTree :: Tree -> SizeTree -> Put
putTree (Node xs) (SNode _ ys) = do
putWord8 $ fromIntegral $ length xs -- number of children
mapM_ (putWord32be . fromIntegral . sz) ys -- sizes of children
sequence_ [putTree x y | (x,y) <- zip xs ys] -- children data
putTree (Leaf xs) _ = do
putWord8 0 -- zero means 'leaf'
putWord32be $ fromIntegral $ length xs -- data length
mapM_ putWord32be xs -- leaf data
mkSizeTree :: Tree -> SizeTree
mkSizeTree (Leaf xs) = SLeaf (1 + 4 + 4 * length xs)
mkSizeTree (Node xs) = SNode (1 + 4 * length xs + sum' (map sz ys)) ys
where
ys = map mkSizeTree xs
sum' = foldl' (+) 0
重要的是要防止GHC将两个通道合并为一个(在这种情况下,它将在内存中保存树)。 这里是通过将非树而不是树生成器提供给函数来完成的。
serialize mkTree size = runPut $ putTree (mkTree size) treeSize
where
treeSize = mkSizeTree $ mkTree size
main = L.writeFile "dump.bin" $ serialize makeTree 10
答案 3 :(得分:2)
这是使用Builder的实现,它是“二进制”包的一部分。我没有正确地对它进行分析,但是根据“top”它会立即分配108 MB,然后在执行其余部分时挂起它。
请注意,我还没有尝试过读取数据,因此我的尺寸和偏移计算可能会出现潜伏错误。
-- Paste this into TreeBinary.hs, and compile with
-- ghc -O2 --make TreeBinary.hs -o TreeBinary
module Main where
import qualified Data.ByteString.Lazy as BL
import qualified Data.Binary.Builder as B
import Data.List (init)
import Data.Monoid
import Data.Word
-- -------------------------------------------------------------------
-- Test data.
data Tree = Node [Tree] | Leaf [Word32] deriving Show
-- Approximate size in memory (ignoring laziness) I think is:
-- 101 * 4^9 * sizeof(Int) + 1/3 * 4^9 * sizeof(Node)
-- This version uses [Word32] instead of [Int] to avoid having to write
-- a builder for Int. This is an example of lazy programming instead
-- of lazy evaluation.
makeTree :: Tree
makeTree = makeTree1 9
where makeTree1 0 = Leaf [0..100]
makeTree1 n = Node [ makeTree1 $ n - 1
, makeTree1 $ n - 1
, makeTree1 $ n - 1
, makeTree1 $ n - 1 ]
-- --------------------------------------------------------------------
-- The actual serialisation code.
-- | Given a tree, return a builder for it and its estimated length in bytes.
serialiseTree :: Tree -> (B.Builder, Word32)
serialiseTree (Leaf ns) = (mconcat (B.singleton 2 : map B.putWord32be ns), fromIntegral $ 4 * length ns + 1)
serialiseTree (Node ts) = (mconcat (B.singleton 1 : map B.putWord32be offsets ++ branches),
baseLength + sum subLengths)
where
(branches, subLengths) = unzip $ map serialiseTree ts
baseLength = fromIntegral $ 1 + 4 * length ts
offsets = init $ scanl (+) baseLength subLengths
main = do
putStrLn $ "Length = " ++ show (snd $ serialiseTree makeTree)
BL.writeFile "test.bin" $ B.toLazyByteString $ fst $ serialiseTree makeTree