使用 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.Map
或 HashMap
)自己构建映射,然后使用它来指定可接受的输入并找到相应的运算符构造函数:
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
和构造函数列表同步。
第三种,也许也是最甜蜜的一种是通过某种额外类型 类 自动生成运算符列表:Bounded
和 Enum
,结合起来,允许枚举像你这样的类型的所有构造函数,以及哪个 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
我正在尝试找出如何以最佳方式解析 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.Map
或 HashMap
)自己构建映射,然后使用它来指定可接受的输入并找到相应的运算符构造函数:
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
和构造函数列表同步。
第三种,也许也是最甜蜜的一种是通过某种额外类型 类 自动生成运算符列表:Bounded
和 Enum
,结合起来,允许枚举像你这样的类型的所有构造函数,以及哪个 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