在 Haskell 中,解析器是 Monad 的一个示例。 Monad 实际上是一个比解析器更广泛的主题,但它们实际上很好地介绍了这个概念。这里是它的工作原理。
在解析器中,您希望能够识别(在 BNF 中)之类的规则
assignment ::= identifier "=" expression
在 Haskell 中你可以这样写:
data Statement =
Assignment Identifier Expression
| Block [Statement] -- A block is a list of statements.
| Conditional Expression Statement Statement
| -- etc.
identifier :: Parser Identifier -- Implementation omitted.
literal :: String -> Parser () -- Implementation omitted.
expression :: Parser Expression -- Implementation omitted.
assignment :: Parser Statement
assignment = do -- Note the "do" here.
ident <- identifier
literal "="
expr <- expression
return (Assignment ident expr) -- "return" doesn't mean what you think.
这体现了赋值解析器是一系列子解析器的想法:首先解析标识符,然后识别“=”,然后解析右侧的表达式。希望您能看到 BNF 如何映射到 Haskell 代码中。
Haskell 中的“do”语法是使用>>= 运算符(称为“bind”)的表达式的语法糖。上面的 assignment 示例将其转化为以下内容(大约:我在这里跳过了有关模式匹配失败的内容)
assignment =
identifier >>= (\ident ->
operator "=" >>= (\dummy ->
expression >>= (\expression -> return (Assignment ident expr))))
每个\(称为“lambda”)都引入了一个匿名函数。 >>= 运算符有两个参数。左侧是 Parser monad 中包含的一些值。右边是一个函数,它接受这个值并返回一个新的包装值。绑定运算符的工作是解开左边的值(这可能涉及一些神奇的副作用)并将其传递给右边的函数。在这种情况下,神奇的副作用包括消耗输入文本。
请注意绑定和匿名函数是如何嵌套在脱糖版本中的。 “do”语法中的每一行都转换为前一行的一个新函数。这意味着最后一个函数可以访问到目前为止所有函数中的所有变量。它是一种用 Haskell 等纯函数式语言对一组赋值进行建模的方法。
我在 cmets 中说过,“返回”并不意味着您认为它意味着什么。在 Haskell 中,它与控制流无关,它只是将一个值包装在一个 monad 中(在本例中为 Parser),本身不会产生任何副作用。
这个特定的解析器会生成结果列表。也就是说,不是在每一步都决定 One True Parse,而是在遇到模棱两可的东西时生成一个结果列表。到目前为止,绑定运算符获取每个结果并将其传递给其右侧的函数,该函数又可能再次生成多个结果。或者解析器可能不会产生任何结果,在这种情况下该分支被放弃。因此,每一步都从一个结果列表开始,下一步给出一个结果列表,然后通过“concat”函数将其折叠回一个平面结果列表。
您问题中给出的Monad 类是标准库的一部分。碰巧有很多有用的函数适用于所有 monad,所以给它们一个通用接口效果很好。对于Parser,绑定运算符的类型为
(>>=) :: Parser a -> (a -> Parser b) -> Parser b
并且return 具有类型
return :: a -> Parser a
冥想这些类型以及我对 bind 和 return 所做的描述,你可能会获得一元的启蒙。