在Haskell中将分层数据结构转换为平面数据结构

时间:2012-10-21 16:22:18

标签: list haskell tree

我正在从如下组织的文本文档中提取一些数据:

- "day 1"
    - "Person 1"
        - "Bill 1"
    - "Person 2"
        - "Bill 2"

我可以把它读成一个看起来像这样的元组列表:

[(0,["day 1"]),(1,["Person 1"]),(2,["Bill 1"]),(1,["Person 2"]),(2,["Bill 2"])]

每个元组的第一项表示标题级别,第二项表示与每个标题相关的信息。

我的问题是,如何获得如下所示的项目列表:

[["day 1","Person 1","Bill 1"],["day 1","Person 2","Bill 2"]]

即。每个最深嵌套项的一个列表,包含来自其上方标题的所有信息。 我得到的最接近的是:

f [] = []
f (x:xs) = row:f rest where
leaves = takeWhile (\i -> fst i > fst x) xs
rest = dropWhile (\i -> fst i > fst x) xs
row = concat $ map (\i -> (snd x):[snd i]) leaves

这给了我这个:

[[["day 1"],["Intro 1"],["day 1"],["Bill 1"],["day 1"],["Intro 2"],["day 1"],["Bill 2"]]]

我希望这个解决方案适用于任何级别。

P.S。我是Haskell的新手。我有一种感觉,我可以/应该使用树来存储数据,但我无法绕过它。我也想不出一个更好的头衔。

2 个答案:

答案 0 :(得分:5)

你应该使用树来存储数据。我将复制Data.Tree的用法:

data Tree a = Node a (Forest a) deriving (Show)

type Forest a = [Tree a]

构建树

现在我们想要获取您的弱类型元组列表,并将其转换为({略})Tree StringParser。只要您需要转换弱类型值并在转换为更强类型之前对其进行验证,就可以使用type YourData = [(Int, [String])] type Parser a = YourData -> Maybe (a, YourData)

YourData

a类型同义词表示您要解析的弱类型。 Parser类型变量是您从解析中检索的值。我们的Maybe类型返回Parser,因为Tree可能会失败。要查看原因,以下输入与有效[(0, ["val1"]), (2, ["val2"])] 不对应,因为它缺少树的第1级:

Parser

如果Parser 成功,它还会返回未使用的输入,以便后续解析阶段可以使用它。

现在,奇怪的是,上面的StateT s Maybe a 类型与众所周知的monad变换器堆栈完全匹配:

StateT s Maybe a ~ s -> Maybe (a, s)

如果您展开underlying implementation of StateT

,可以看到这一点
import Control.Monad.Trans.State.Strict

type Parser a = StateT [(Int, [String])] Maybe a

这意味着我们可以定义:

Monad

如果我们这样做,我们会免费为Applicative类型获取AlternativeParserparseElement :: Int -> Parser String parseElement level = StateT $ \list -> case list of [] -> Nothing (level', strs):rest -> case strs of [str] -> if (level' == level) then Just (str, rest) else Nothing _ -> Nothing 个实例。这使得定义解析器变得非常容易!

首先,我们必须定义一个使用树的单个节点的原始解析器:

Tree

这是我们必须编写的唯一非常重要的代码段,因为它是完整的,处理以下所有极端情况:

  • 列表为空
  • 您的节点中有多个值
  • 元组中的数字与预期深度不匹配

下一部分是事情变得优雅的地方。然后我们可以定义两个相互递归的解析器,一个用于解析Forest,另一个用于解析import Control.Applicative parseTree :: Int -> Parser (Tree String) parseTree level = Node <$> parseElement level <*> parseForest (level + 1) parseForest :: Int -> Parser (Forest String) parseForest level = many (parseTree level)

Applicative

第一个解析器使用StateT样式,因为Applicative免费为我们提供了StateT个实例。但是,我也可以使用Monad的{​​{1}}实例来提供对于命令式程序员更具可读性的代码:

parseTree :: Int -> Parser (Tree String)
parseTree level = do
    str    <- parseElement level
    forest <- parseForest (level + 1)
    return $ Node str forest

但是many函数怎么样?那是做什么的?我们来看看它的类型:

many :: (Alternative f) => f a -> f [a]

它会返回一个值并执行Applicative,而是反复调用它来返回值列表。当我们根据Parser定义State类型时,我们免费获得Alternative个实例,因此我们可以使用many函数转换解析单个Tree的内容{1}}(即parseTree),用于解析Forest(即parseForest)的内容。

要使用我们的Parser,我们只需重命名现有的StateT功能,以明确其目的:

runParser :: Parser a - &gt; [(Int,[String])] - &gt;也许是 runParser = evalStateT

然后我们就跑了!

>>> runParser (parseForest 0) [(0,["day 1"]),(1,["Person 1"]),(2,["Bill 1"]),(1,["Person 2"]),(2,["Bill 2"])]
Just [Node "day 1" [Node "Person 1" [Node "Bill 1" []],Node "Person 2" [Node "Bill 2" []]]]

那真是太神奇了!让我们看看如果我们给它一个无效的输入会发生什么:

>>> runParser (parseForest 0) [(0, ["val1"]), (2, ["val2"])]
Just [Node "val1" []]

一部分输入成功!我们实际上可以通过定义与输入结尾匹配的解析器来指定它必须使用整个输入:

eof :: Parser ()
eof = StateT $ \list -> case list of
    [] -> Just ((), [])
    _  -> Nothing

现在让我们试一试:

>>> runParser (parseForest 0 >> eof) [(0, ["val1"]), (2, ["val2"])]
Nothing

完美!

展平树

要回答第二个问题,我们再次使用相互递归函数解决问题:

flattenForest :: Forest a -> [[a]]
flattenForest forest = concatMap flattenTree forest

flattenTree :: Tree a -> [[a]]
flattenTree (Node a forest) = case forest of
    [] -> [[a]]
    _ -> map (a:) (flattenForest forest)

我们试试吧!

>>> flattenForest [Node "day 1" [Node "Person 1" [Node "Bill 1" []],Node "Person 2" [Node "Bill 2" []]]]
[["day 1","Person 1","Bill 1"],["day 1","Person 2","Bill 2"]]

现在,从技术上讲,我不必使用相互递归的函数。我本可以做一个递归函数。我只关注TreeData.Tree类型的定义。

结论

所以理论上我可以通过跳过中间Tree类型并直接解析扁平化结果来进一步缩短代码,但我想你可能想要使用基于Tree的表示形式其他目的。

关键的主要观点是:

  • 了解Haskell抽象以简化代码
  • 始终编写总功能
  • 学会有效使用递归

如果你这样做,你将编写完全符合问题的强大而优雅的代码。

附录

以下是包含我所说的所有内容的最终代码:

import Control.Applicative
import Control.Monad.Trans.State.Strict
import Data.Tree

type YourType = [(Int, [String])]

type Parser a = StateT [(Int, [String])] Maybe a

runParser :: Parser a -> [(Int, [String])] -> Maybe a
runParser = evalStateT

parseElement :: Int -> Parser String
parseElement level = StateT $ \list -> case list of
    []                  -> Nothing
    (level', strs):rest -> case strs of
        [str] ->
            if (level' == level)
            then Just (str, rest)
            else Nothing
        _     -> Nothing

parseTree :: Int -> Parser (Tree String)
parseTree level = Node <$> parseElement level <*> parseForest (level + 1)

parseForest :: Int -> Parser (Forest String)
parseForest level = many (parseTree level)

eof :: Parser ()
eof = StateT $ \list -> case list of
    [] -> Just ((), [])
    _  -> Nothing

flattenForest :: Forest a -> [[a]]
flattenForest forest = concatMap flattenTree forest

flattenTree :: Tree a -> [[a]]
flattenTree (Node a forest) = case forest of
    [] -> [[a]]
    _  -> map (a:) (flattenForest forest)

答案 1 :(得分:3)

我好像已经解决了。

group :: [(Integer, [String])] -> [[String]]
group ((n, str):ls) = let
      (children, rest) = span (\(m, _) -> m > n) ls
      subgroups = map (str ++) $ group children
   in if null children then [str] ++ group rest
      else subgroups ++ group rest
group [] = []

虽然我没有测试过。

想法是注意递归模式。此函数获取列表的第一个元素(N,S),然后将更高级别的所有条目收集到级别为N的另一个元素,并将其收集到列表“children”中。如果没有孩子,我们处于顶层,S形成输出。如果有一些,则将S附加到所有这些。

至于为什么你的算法不起作用,问题主要在row。请注意,您没有递归下降。


也可以使用树木。

data Tree a = Node a [Tree a] deriving Show

listToTree :: [(Integer, [String])] -> [Tree [String]]
listToTree ((n, str):ls) = let
      (children, rest) = span (\(m, _) -> m > n) ls
      subtrees = listToTree children
   in Node str subtrees : listToTree rest
listToTree [] = []

treeToList :: [Tree [String]] -> [[String]]
treeToList (Node s ns:ts) = children ++ treeToList ts where
   children = if null ns then [s] else map (s++) (treeToList ns)
treeToList [] = []

算法基本相同。上半部分转到第一部分,第二部分转到第二部分。