使用 parsec 解析 sum 数据类型

Parsing a sum datatype with parsec

我正在尝试找出如何以最佳方式解析 Haskell 中的总和数据类型。这是我尝试的摘录

type Value = Int

data Operator = ADD | SUB | MUL | DIV | SQR deriving (Show)

toOperator :: String -> Maybe Operator
toOperator "ADD" = Just ADD
toOperator "SUB" = Just SUB
toOperator "MUL" = Just MUL
toOperator "DIV" = Just DIV
toOperator "SQR" = Just SQR
toOperator _     = Nothing

parseOperator :: ParsecT String u Identity () Operator
parseOperator = do
    s <- choice $ map (try . string) ["ADD", "SUB", "MUL", "DIV", "SQR"]
    case toOperator s of
        Just x  -> return x
        Nothing -> fail "Could not parse that operator."

这段代码做了我想要的,但有一个明显的问题:它检查数据两次。一次进入 choice $ map (try . string) ["ADD", "SUB", "MUL", "DIV", "SQR"] 行,一次通过 toOperator

我想要的是,如果字符串出现在列表中,则将其解析为 Operator,否则失败。但我不知道如何以 'clean' 的方式做到这一点。

你只需要 toOperator 的逆函数来映射解析器; read 是一个简单的(如果不是健壮的)示例。

>>> data Operator = ADD | SUB | MUL | DIV | SQR deriving (Show, Read)
>>> parse (read <$> string "ADD") "" "ADD" :: Either ParseError Operator
Right ADD

您有多种选择可以避免此类重复。

首先,如果您尝试解析的输入中出现的名称与 Operator 的构造函数完全匹配(在您的示例中似乎就是这种情况),您可以避免 toOperator完全通过为 Operator 派生 Read 实例并仅使用 read。代码将遵循

parseOperator :: ParsecT String u Identity () Operator
parseOperator = do
    s <- choice $ map (try . string) ["ADD", "SUB", "MUL", "DIV", "SQR"]
    pure $ read s

不过,您必须小心地在此处列出与 Operator 构造函数相同的名称,并根据需要更新它们。

其次,您可以通过定义列表(或 Data.MapHashMap)自己构建映射,然后使用它来指定可接受的输入并找到相应的运算符构造函数:

operators :: [(String, Operator)]
operators = [("ADD", ADD), ("SUB", SUB), ("MUL", MUL), ("DIV", DIV), ("SQR", SQR)]

parseOperator :: ParsecT String u Identity () Operator
parseOperator = do
    s <- choice $ map (try . string . fst) operators
    case lookup s operators of
        Just x  -> return x
        Nothing -> fail "Could not parse that operator."

请注意 case 对于 well-defined 解析器来说并不是真正必需的:根据定义,解析的结果将在 operators 列表中。而且,缺点是您必须保持 operators 和构造函数列表同步。

第三种,也许也是最甜蜜的一种是通过某种额外类型 类 自动生成运算符列表:BoundedEnum,结合起来,允许枚举像你这样的类型的所有构造函数,以及哪个 ghc 将很乐意为你的 Operator 派生。然后 operators 定义看起来像

operators :: [(String, Operator)]
operators = map (\op -> (show op, op)) $ enumFromTo minBound maxBound

如果让 toOperator 直接参与 Parsec 解析过程会更简单,而不是让它成为一个单独发生的步骤,因为这样 "whether this thing is a valid operator" 可以为解析过程提供反馈。

对于这种特定情况,您正在解析的是一个 zero-field 枚举,其构造函数名称与您正在解析的字符串完全匹配,已经发布了几个很好的快捷方式,向您展示了如何简洁地解析这些构造函数.在这个答案中,我将展示另一种方法,它更容易适应 "match one of several cases" 的一般情况并处理更高级的东西,如 "one of the three constructors has an Int argument but the others don't."

operator :: StringParser Operator
operator = string "ADD" *> pure ADD
       <|> string "DIV" *> pure DIV 
       <|> string "MUL" *> pure MUL
       <|> try (string "SUB") *> pure SUB 
       <|> string "SQR" *> pure SQR

现在假设您有一个额外的构造函数,VAR,接受一个 String 参数。很容易向这个解析器添加对该构造函数的支持:

operator :: StringParser Operator
operator = ...
       <|> string "VAR" *> (VAR <$> var)

var :: StringParser String
var = spaces *> anyChar `manyTill` space