使用Haskell Parsec解析表达式时,标识符与运算符无空格的解析问题
解决Parsec中标识符与运算符无空格的解析问题
这种情况我之前在写Parsec解析器时也碰到过,核心问题在于Parsec本身不会自动跳过空白字符——不管是空格、制表符还是换行,都需要你显式处理。当你写a*a时,解析器在读完第一个a后,没有意识到后面的*是运算符,反而可能认为表达式已经结束了,这就是为什么只得到第一个标识符。
下面给你两种靠谱的解决方案,从手动实现到用现成工具都有:
方案1:手动实现空白处理组合子
这种方式更灵活,适合自定义规则:
首先,先定义空白解析器和辅助组合子,让每个token解析后自动跳过后续空白:
import Text.Parsec import Text.Parsec.String (Parser) -- 定义要忽略的空白字符:空格、制表符、换行 whiteSpace :: Parser () whiteSpace = skipMany (oneOf " \t\n") -- lexeme:解析一个token,然后跳过后面的所有空白 lexeme :: Parser a -> Parser a lexeme p = p <* whiteSpace -- symbol:解析指定运算符/符号,自动跳过后续空白 symbol :: String -> Parser String symbol s = lexeme (string s)
然后定义标识符和运算符的解析器:
-- 标识符:字母/下划线开头,后面跟字母/数字/下划线 identifier :: Parser String identifier = lexeme $ do first <- letter <|> char '_' rest <- many (letter <|> digit <|> char '_') return (first : rest) -- 乘法运算符,用symbol包裹 multOp :: Parser String multOp = symbol "*"
最后是表达式解析器,用chainl1处理左结合的二元运算(比如乘法):
-- 定义表达式的数据类型 data Expr = Identifier String | BinOp String Expr Expr deriving (Show) expr :: Parser Expr expr = term `chainl1` parseMultOp where -- 解析乘法运算符,返回对应的表达式构造函数 parseMultOp = do op <- multOp return (\a b -> BinOp op a b) -- 基础项:标识符或括号表达式 term :: Parser Expr term = Identifier <$> identifier <|> parens expr -- 括号表达式,需要的话可以加 where parens p = char '(' *> p <* char ')' <* whiteSpace
这样修改后,不管是a * a还是a*a,解析器都会正确识别为BinOp "*" (Identifier "a") (Identifier "a")。
方案2:使用Parsec的Token模块(更省心)
Parsec提供了Text.Parsec.Token模块,内置了成熟的词法分析工具,能自动处理空白、标识符、运算符等,适合快速开发:
import Text.Parsec import Text.Parsec.String (Parser) import qualified Text.Parsec.Token as Tok import Text.Parsec.Language (emptyDef) -- 定义语言规则:自定义标识符、运算符的格式 langDef :: Tok.LanguageDef () langDef = emptyDef { Tok.identStart = letter <|> char '_' -- 标识符开头 , Tok.identLetter = letter <|> digit <|> char '_' -- 标识符后续字符 , Tok.opStart = oneOf "*+-/" -- 运算符开头 , Tok.opLetter = oneOf "*+-/" -- 运算符后续字符(这里单字符运算符可以和opStart一致) } -- 创建词法解析器 lexer :: Tok.TokenParser () lexer = Tok.makeTokenParser langDef -- 从lexer中直接获取现成的解析器 identifier :: Parser String identifier = Tok.identifier lexer multOp :: Parser String multOp = Tok.operator lexer "*" -- 表达式解析 data Expr = Identifier String | BinOp String Expr Expr deriving (Show) expr :: Parser Expr expr = do Tok.whiteSpace lexer -- 先跳过开头的所有空白 term `chainl1` parseMultOp where parseMultOp = do op <- multOp return (\a b -> BinOp op a b) term :: Parser Expr term = Identifier <$> identifier <|> Tok.parens lexer expr -- 括号表达式,自动处理括号前后的空白
这个方案的好处是不用自己手动处理空白的细节,Token模块会帮你搞定所有token之间的空白(包括零空白的情况)。
关键要点总结
- 空白必须显式处理:Parsec不会默认跳过空白,所以每个token(标识符、运算符)都要确保前后的空白被正确忽略。
- 用chainl1处理链式运算:如果你的表达式支持连续的二元运算(比如
a*b*c),chainl1会自动循环解析运算符和后续的操作数,不会只停在第一个标识符。 - 区分token和非token:确保运算符不会被误解析为标识符的一部分(比如不要把
*加入标识符的允许字符列表)。
你可以根据自己的需求选择上面的方案,测试一下a*a和a * a,应该都能得到正确的表达式树了。
内容的提问来源于stack exchange,提问作者DylanSp
相关产品推荐
相关产品推荐

