树木
你是对的,你应该使用树来存储数据。我将复制Data.Tree
它是如何做到的:
data Tree a = Node a (Forest a) deriving (Show)
type Forest a = [Tree a]
构建树
现在我们想要获取您的弱类型元组列表并将其转换为(稍微)更强Tree
的String
s。任何时候您需要在转换为强类型之前转换弱类型值并对其进行验证,您都可以使用Parser
:
type YourData = [(Int, [String])]
type Parser a = YourData -> Maybe (a, YourData)
YourData
类型同义词表示您正在解析的弱类型。a
类型变量是您从解析中检索的值。我们的Parser
类型返回 aMaybe
因为Parser
可能会失败。要了解原因,以下输入与有效的 不对应Tree
,因为它缺少树的第 1 级:
[(0, ["val1"]), (2, ["val2"])]
如果Parser
确实成功,它还会返回未使用的输入,以便后续解析阶段可以使用它。
现在,奇怪的是,上面的Parser
类型完全匹配一个众所周知的 monad 转换器堆栈:
StateT s Maybe a
如果您扩展以下的底层实现,StateT
您可以看到这一点:
StateT s Maybe a ~ s -> Maybe (a, s)
这意味着我们可以定义:
import Control.Monad.Trans.State.Strict
type Parser a = StateT [(Int, [String])] Maybe a
如果我们这样做,我们将免费获得我们类型的Monad
,Applicative
和Alternative
实例。Parser
这使得定义解析器变得非常容易!
首先,我们必须定义一个使用树的单个节点的原始解析器:
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
这是我们必须编写的唯一一段重要的代码,因为它是完整的,它可以处理以下所有极端情况:
- 列表为空
- 您的节点中有多个值
- 元组中的数字与预期深度不匹配
下一部分是事情变得非常优雅的地方。然后我们可以定义两个相互递归的解析器,一个用于解析 a Tree
,另一个用于解析 a 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
实例来为命令式程序员提供更具可读性的代码:
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
(ie parseTree
) 的内容转换为解析 a Forest
(ie parseForest
) 的内容。
要使用我们的Parser
,我们只需重命名现有StateT
函数以明确其用途:
runParser :: Parser a -> [(Int, [String])] -> 也许一个 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"]]
现在,从技术上讲,我不必使用相互递归函数。我本可以完成一个递归函数。我只是按照Tree
.Data.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)