You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

结构建议

  1. 从原子到组合:先实现最小单元的解析器(比如整数、单个符号),再逐步组合成复杂结构,避免一开始就处理最外层的大列表。
  2. 封装重复逻辑:用lexer、betweenParens这类工具函数减少重复代码,同时统一处理空格,避免手动到处添加spaces。
  3. 明确上下文规则:比如Interval的生成规则(比较用闭区间、运算用对称区间)要提前明确,再对应到解析逻辑中。
  4. 优先匹配长符号:解析运算符时,先匹配长度更长的符号(比如>=要在>之前),避免截断解析错误。

测试示例

用你的输入字符串测试:

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 04:55:36