如何在Haskell中将树数据结构保存到二进制文件中

时间:2011-03-01 17:21:49

标签: haskell functional-programming binary-tree monads monad-transformers

我正在尝试使用Haskell将一个简单(但相当大)的Tree结构保存到二进制文件中。结构看起来像这样:

-- For simplicity assume each Node has only 4 childs
data Tree = Node [Tree] | Leaf [Int]
以下是我需要在磁盘上查看数据的方式:

  1. 每个节点以其子节点的四个32位偏移开始,然后跟随孩子。
  2. 我不太关心叶子,让我们说它只是n个连续的32位数字。
  3. 出于实际目的,我需要一些节点标签或一些其他附加数据 但是现在我也不在乎这么多。
  4. 对我来说,Haskellers在编写二进制文件时的第一选择是Data.Binary.Put库。但有了这个,我在子弹#1中遇到了问题。特别是,当我要将一个Node写入文件时,为了记下子偏移,我需要知道我当前的偏移量和每个孩子的大小。

    这不是Data.Binary.Put所提供的,所以我认为这必须是Monad变换器的完美应用。但即使它看起来很酷且功能齐全,到目前为止我还没有成功使用这种方法。

    我问了另外两个我认为可以帮助我解决问题的问题herehere。我必须说,每次我收到非常好的答案,帮助我进一步发展,但不幸的是,我仍然无法解决整个问题。

    Here是我到目前为止所做的,它仍然会泄漏太多内存而不实用。

    我很想拥有使用这种功能方法的解决方案,但也会对任何其他解决方案表示感谢。

4 个答案:

答案 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)

另一个选项是,在序列化节点之后,返回并使用适当的数据重写偏移字段。如果树很大,这可能是唯一的选择,但我不知道支持这个的序列化库。这将涉及在IOseek工作到正确的位置。

答案 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