如何在算术表达式中解析其他表达式?(Haskell解析器问题)
解决Haskell解析器中算术表达式与普通表达式的递归循环问题
嘿,我之前写表达式解析器的时候也踩过这个递归循环的坑,咱们一步步拆解解决它~
首先得揪出问题根源:你原来的设计里,expressionParser包含了AExprAsExpr(把算术表达式包装成普通表达式),现在又想让aTerm(算术表达式的原子项)调用expressionParser,这就形成了循环依赖——expressionParser依赖aExprParser,aExprParser依赖aTerm,aTerm又依赖expressionParser,解析器直接陷入无限递归。另外,标识符被错误包裹在AExprAsExpr里,是因为解析器优先把标识符当成了算术表达式的一部分,没直接解析成普通表达式的标识符项。
核心思路:拆分解析器层级,明确边界
我们需要把解析器拆成三个层级,既打破循环,又清晰划分基础项与组合表达式:
- 原子表达式解析器:处理最基础的普通表达式单元,是所有表达式的底层依赖,不碰算术表达式。
- 算术表达式解析器:建立在原子表达式之上,专门处理算术运算逻辑。
- 完整表达式解析器:整合算术表达式(包装成
AExprAsExpr)和其他普通表达式。
具体代码实现示例
假设你的表达式类型大概是这样:
data Expr = Identifier String | MethodCall String [Expr] | AExprAsExpr AExpr -- 其他普通表达式构造器... deriving (Show) data AExpr = ANumber Integer | AVar Expr -- 用普通表达式作为算术变量载体 | AAdd AExpr AExpr | AMul AExpr AExpr -- 其他算术表达式构造器... deriving (Show)
第一步:定义原子表达式解析器
这部分处理最基础的普通表达式单元,比如标识符、方法调用、括号包裹的完整表达式:
import Text.Parsec import Text.Parsec.String (Parser) primaryExprParser :: Parser Expr primaryExprParser = choice [ -- 直接解析标识符为普通Expr,不会被包装成算术表达式 Identifier <$> identifierParser -- 解析方法调用,参数支持完整表达式(递归调用expressionParser是安全的,括号是明确终止符) , MethodCall <$> identifierParser <*> parens (commaSep expressionParser) -- 括号里的完整表达式 , parens expressionParser -- 可添加其他原子项,比如布尔字面量、常量等 ] where identifierParser = (:) <$> letter <*> many (alphaNum <|> char '_')
第二步:修改算术表达式解析器,依赖原子表达式
让aTerm基于primaryExprParser构建,不再依赖整个expressionParser:
-- 定义算术运算符的优先级与结合性 aOperators :: [[Operator String () AExpr]] aOperators = [ [Infix (AMul <$ char '*') AssocLeft] , [Infix (AAdd <$ char '+') AssocLeft] ] aTerm :: Parser AExpr aTerm = choice [ -- 把原子表达式转成算术变量(AVar) AVar <$> primaryExprParser -- 解析数字字面量 , ANumber <$> read <$> many1 digit -- 括号里的算术表达式 , parens aExprParser ] aExprParser :: Parser AExpr aExprParser = buildExpressionParser aOperators aTerm
第三步:整合完整表达式解析器
现在expressionParser可以安全地整合算术表达式和普通原子表达式,彻底打破循环:
expressionParser :: Parser Expr expressionParser = choice [ -- 把算术表达式包装成普通Expr AExprAsExpr <$> aExprParser -- 普通原子表达式(标识符、方法调用等) , primaryExprParser -- 可添加其他普通表达式类型,比如赋值、条件表达式等 ]
为什么这样能解决问题?
- 打破循环依赖:
expressionParser依赖aExprParser,aExprParser依赖primaryExprParser,primaryExprParser仅在括号场景递归调用expressionParser——这是合法递归,因为括号提供了明确的语法边界,不会无限循环。 - 避免错误包裹:标识符会被
primaryExprParser直接解析成Identifier Expr,只有在解析算术表达式时,才会把原子表达式转成AVar AExpr,再通过AExprAsExpr包装成Expr,完全符合预期逻辑。
额外小提示
如果你的语法里算术表达式是普通表达式的子集(比如x+y本身就是合法普通表达式),要注意解析顺序:expressionParser里先尝试解析aExprParser,再解析primaryExprParser,这样复杂的算术表达式会被优先识别,不会被拆成单个标识符和运算符。
内容的提问来源于stack exchange,提问作者Michael
相关产品推荐
相关产品推荐

