Haskell中使用readP库构建左结合算术解析器的问题
解决ReadP构建左结合算术解析器的问题
你的核心问题在于递归下降解析器无法直接处理左递归语法,同时当前的右递归写法自然会产生右结合的AST。要实现左结合,需要通过语法分层+迭代折叠的方式改造解析器结构。
问题根源分析
- 当你把
pOp里的e1改成pExpr时,会触发左递归:pExpr调用pOp,pOp又调用pExpr,导致无限循环,这是递归下降解析器的固有局限。 - 当前代码中
pOp用numConst作为左操作数,pExpr作为右操作数,本质是右递归语法,所以解析2+4+6+8时会生成2+(4+(6+8))这种右结合结构。
解决方案:语法分层+左折叠
正确的做法是将表达式拆分为原子表达式和后缀运算符链,通过迭代收集所有运算符和右操作数,再用左折叠将它们组合成左结合的AST。
修改后的完整代码:
import Data.Char import Text.ParserCombinators.ReadP import Control.Applicative ((<|>)) type Parser a = ReadP a data Value = IntVal Int deriving (Eq, Show, Read) data Exp = Const Value | Oper Op Exp Exp deriving (Eq, Show, Read) data Op = Plus deriving (Eq, Show, Read) space :: Parser Char space = satisfy isSpace spaces :: Parser String spaces = many space space1 :: Parser String space1 = many1 space symbol :: String -> Parser String symbol = token . string token :: Parser a -> Parser a token combinator = do spaces combinator parseString input = readP_to_S (do e <- pExpr token eof return e) input -- 原子表达式:最基础的表达式单元(这里是数字,后续可扩展括号表达式) pAtom :: Parser Exp pAtom = do skipSpaces digits <- munch isDigit return $ Const (IntVal (read digits)) -- 主表达式:先解析一个原子,再迭代解析后续的运算符+原子,左折叠实现左结合 pExpr :: Parser Exp pExpr = do first <- pAtom rest <- many parseOpAndAtom return $ foldl combine first rest where combine exp1 (op, exp2) = Oper op exp1 exp2 parseOpAndAtom = do skipSpaces _ <- symbol "+" exp2 <- pAtom return (Plus, exp2)
关键代码解释
- pAtom:负责解析最基础的表达式单元(比如整数常量),这是递归的终止点,避免左递归。
- pExpr:
- 先解析第一个原子表达式作为初始值。
- 用
many迭代解析所有后续的+ 原子组合,得到一个[(Op, Exp)]列表。 - 用
foldl左折叠这个列表:每次将当前的左结合表达式与新的右操作数组合,最终生成((2+4)+6)+8这种左结合的AST。
验证效果
在GHCi中测试:
ghci> parseString "2+4+6+8" [((((Const (IntVal 2) `Oper` Plus) (Const (IntVal 4)) `Oper` Plus) (Const (IntVal 6)) `Oper` Plus) (Const (IntVal 8)),"")]
输出的AST是标准的左结合形式。
为什么不需要用look前瞻
你尝试的look方案是绕远路,根本问题是语法结构设计不当,而非需要前瞻处理。通过语法分层消除左递归,再用迭代折叠实现左结合,是递归下降解析器处理这类问题的标准方案。
内容的提问来源于stack exchange,提问作者Piskator
相关产品推荐
相关产品推荐

