如何用Parsec解析元组列表字符串为自定义Haskell数据类型?
Parsec解析自定义数据类型的构建方案与结构
第一步:明确自定义数据类型定义
根据你的示例输入输出,先补全Haskell数据类型定义(确保与解析目标匹配):
import Text.Parsec import Text.Parsec.String (Parser) import Text.Parsec.Language (emptyDef) import qualified Text.Parsec.Token as Tok -- 基础自定义类型 data Reg = Reg Int deriving (Show, Eq) data Interval = Intv (Int, Int) deriving (Show, Eq) data BinAlu = Add | Sub | Mul | Mod deriving (Show, Eq) -- %对应Mod data Jcmp = Jlt | Jle | Jgt | Jge deriving (Show, Eq) -- <对应Jlt,>=对应Jge data LF = Cmp (Jcmp, Interval) | Op (BinAlu, Interval) deriving (Show, Eq) -- 目标解析类型 type Target = [(Reg, [LF])]
第二步:构建基础工具解析器
封装处理空格和符号的工具,避免重复处理空格逻辑:
-- 定义词法分析器,自动跳过空格 lexer :: Tok.TokenParser () lexer = Tok.makeTokenParser emptyDef -- 解析符号并自动跳过后续空格 symbol :: String -> Parser String symbol = Tok.symbol lexer -- 解析整数并自动跳过后续空格 intParser :: Parser Int intParser = Tok.integer lexer >>= return . fromIntegral -- 包裹在圆括号内的解析器 betweenParens :: Parser a -> Parser a betweenParens = between (symbol "(") (symbol ")") -- 包裹在方括号内的解析器 betweenBrackets :: Parser a -> Parser a betweenBrackets = between (symbol "[") (symbol "]")
第三步:逐个构建组件解析器
1. Reg解析器
将单个数字解析为Reg类型:
regParser :: Parser Reg regParser = Reg <$> intParser
2. Jcmp(比较运算符)解析器
将字符串符号映射到Jcmp枚举类型,优先匹配长符号(比如先匹配>=再匹配>,避免解析错误):
jcmpParser :: Parser Jcmp jcmpParser = choice [ symbol "<=" >> return Jle , symbol "<" >> return Jlt , symbol ">=" >> return Jge , symbol ">" >> return Jgt ]
3. BinAlu(二进制运算)解析器
将字符串符号映射到BinAlu枚举类型:
binAluParser :: Parser BinAlu binAluParser = choice [ symbol "+" >> return Add , symbol "-" >> return Sub , symbol "*" >> return Mul , symbol "%" >> return Mod ]
4. Interval解析器
根据上下文生成对应区间:
- 比较场景(Cmp):单个数字
n→Intv (n, n) - 运算场景(Op):单个数字
n→Intv (-n, n)
-- 比较用的Interval解析器 cmpIntervalParser :: Parser Interval cmpIntervalParser = Intv <$> (\n -> (n, n)) <$> intParser -- 运算用的Interval解析器 opIntervalParser :: Parser Interval opIntervalParser = Intv <$> (\n -> (-n, n)) <$> intParser
5. LF解析器
解析两种LF类型,通过匹配不同符号区分:
lfParser :: Parser LF lfParser = betweenParens $ choice [ -- 解析Cmp类型:(比较符号, 数字) Cmp <$> ((,) <$> jcmpParser <*> (symbol "," *> cmpIntervalParser)) , -- 解析Op类型:(运算符号, 数字) Op <$> ((,) <$> binAluParser <*> (symbol "," *> opIntervalParser)) ]
6. LF列表解析器
解析方括号包裹的LF列表,元素用逗号分隔:
lfListParser :: Parser [LF] lfListParser = betweenBrackets $ sepBy lfParser (symbol ",")
7. (Reg, [LF])对解析器
解析圆括号包裹的Reg与LF列表的配对:
regLfPairParser :: Parser (Reg, [LF]) regLfPairParser = betweenParens $ (,) <$> regParser <*> (symbol "," *> lfListParser)
第四步:顶层解析器
解析整个目标类型列表,同时处理开头结尾的空格和输入结束:
topParser :: Parser Target topParser = betweenBrackets $ sepBy regLfPairParser (symbol ",") -- 对外暴露的解析函数,处理输入字符串 parseTarget :: String -> Either ParseError Target parseTarget input = parse (spaces *> topParser <* spaces <* eof) "" input
结构建议
- 从原子到组合:先实现最小单元的解析器(比如整数、单个符号),再逐步组合成复杂结构,避免一开始就处理最外层的大列表。
- 封装重复逻辑:用
lexer、betweenParens这类工具函数减少重复代码,同时统一处理空格,避免手动到处添加spaces。 - 明确上下文规则:比如Interval的生成规则(比较用闭区间、运算用对称区间)要提前明确,再对应到解析逻辑中。
- 优先匹配长符号:解析运算符时,先匹配长度更长的符号(比如
>=要在>之前),避免截断解析错误。
测试示例
用你的输入字符串测试:
testInput :: String testInput = "[(1,[(<,0),(%,4)]), (2,[>=, 4])]" main :: IO () main = case parseTarget testInput of Left err -> print err Right result -> print result
输出会与你预期的一致:
[(Reg 1,[Cmp (Jlt,Intv (0,0)),Op (Mod,Intv (-4,4))]),(Reg 2,[Cmp (Jge,Intv (4,4))])]
内容的提问来源于stack exchange,提问作者wardz
相关产品推荐
相关产品推荐

