【问题标题】:Converting a hierarchical data structure to a flat one in Haskell在 Haskell 中将分层数据结构转换为平面结构
【发布时间】:2012-10-21 16:22:18
【问题描述】:

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

- "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"]]]

我希望该解决方案适用于任意数量的级别。

附:我是 Haskell 的新手。我有一种感觉,我可以/应该使用一棵树来存储数据,但我无法绕过它。我也想不出更好的标题。

【问题讨论】:

    标签: list haskell tree


    【解决方案1】:

    树木

    您是对的,您应该使用树来存储数据。我会复制Data.Tree 的做法:

    data Tree a = Node a (Forest a) deriving (Show)
    
    type Forest a = [Tree a]
    

    构建树

    现在我们想要获取您的弱类型元组列表并将其转换为(稍微)更强的TreeStrings。任何时候您需要转换弱类型值并在转换为更强类型之前对其进行验证,您可以使用Parser

    type YourData = [(Int, [String])]
    
    type Parser a = YourData -> Maybe (a, YourData)
    

    YourData 类型同义词表示您正在解析的弱类型。 a 类型变量是您从解析中检索的值。我们的Parser 类型返回Maybe,因为Parser 可能会失败。要了解原因,以下输入与有效的 Tree 不对应,因为它缺少树的第 1 级:

    [(0, ["val1"]), (2, ["val2"])]
    

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

    现在,奇怪的是,上面的 Parser 类型完全匹配一个众所周知的 monad 转换器堆栈:

    StateT s Maybe a
    

    如果你展开underlying implementation of StateT,你可以看到这个:

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

    这意味着我们可以定义:

    import Control.Monad.Trans.State.Strict
    
    type Parser a = StateT [(Int, [String])] Maybe a
    

    如果我们这样做,我们将免费获得Parser 类型的MonadApplicativeAlternative 实例。这使得定义解析器变得非常容易!

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

    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
    

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

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

    下一部分是事情变得非常优雅的地方。然后我们可以定义两个相互递归的解析器,一个用于解析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 实例。但是,我也可以改用StateTMonad 实例,为命令式程序员提供更易读的代码:

    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 的东西,而是重复调用它以返回值列表。当我们根据State 定义Parser 类型时,我们得到了一个免费的Alternative 实例,因此我们可以使用many 函数来转换解析单个Tree 的东西(即parseTree) ,转化为解析Forest(即parseForest)的东西。

    要使用我们的Parser,我们只需重命名现有的StateT 函数以明确其用途:

    runParser :: Parser a -> [(Int, [String])] -> Maybe a 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"]]
    

    现在,从技术上讲,我不必使用相互递归函数。我本可以完成一个递归函数。我只是遵循Data.TreeTree 类型的定义。

    结论

    所以理论上我可以通过跳过中间 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)
    

    【讨论】:

    • 如何从元组列表中构造树? flatten 并不能完全满足我的要求:我希望每个嵌套最多的项目都有一个列表,其中包含该项目及其上方的所有项目。
    • Data.Tree 对于这种数据结构来说是一个不错的库:)
    • @ajerneck 我更新了它。我改用Data.Tree 中的类型,向您展示了如何将数据类型转换为该树,并修复了展平功能以匹配您的问题描述。它现在可以满足您的要求。
    • 感谢您的出色回答。最后我接受了另一个答案,因为那是我用来解决眼前问题的方法,一旦解决了,我就不得不在没有实际实现树的情况下继续做其他事情。
    • 我终于按照这个例子实现了一个解析器(对于另一个问题,但仍然如此)。
    【解决方案2】:

    我好像已经解决了。

    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 的另一个元素,并将其放入列表“孩子”中。如果没有孩子,我们就在顶层,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 [] = []
    

    算法本质上是一样的。前半部分转到第一个函数,后半部分转到第二个函数。

    【讨论】:

    • 据我所知,这确实符合我的要求。太感谢了!!我可以告诉我需要递归到孩子身上,但我不知道该怎么做。我还是对基于树的解决方案很好奇,所以我在添加树标签并稍等片刻,否则我会接受这个答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多