21

我对 Scala 相当陌生,在阅读解析器组合器(解析器组合器背后的魔力Scala 中的域特定语言)时,我遇到了这样的方法定义:

def classPrefix = "class" ~ ID ~ "(" ~ formals ~ ")"

我一直在阅读 scala.util.parsing.Parsers 的 API 文档,它定义了一个名为 (tilde) 的方法,但我仍然不太了解上面示例中的用法。在该示例中(波浪号)是一个在 java.lang.String 上调用的方法,它没有该方法并导致编译器失败。我知道(波浪号)被定义为

case class ~ [+a, +b] (_1: a, _2: b)

但这对上面的例子有什么帮助?

如果有人可以给我一个提示以了解这里发生了什么,我会很高兴。非常感谢您!

4

3 回答 3

30

这里的结构有点棘手。首先,请注意,您总是某个解析器的子类中定义这些东西,例如class MyParser extends RegexParsers. 现在,您可能会注意到其中的两个隐式定义RegexParsers

implicit def literal (s: String): Parser[String]
implicit def regex (r: Regex): Parser[String]

这些将做的是获取任何字符串或正则表达式并将它们转换为与该字符串或该正则表达式匹配的解析器作为标记。它们是隐式的,因此它们将在需要时随时应用(例如,如果您调用Parser[String]String(或Regex)没有的方法)。

但这是什么Parser东西?它是内部定义的内部类Parsers,超特征为RegexParser

class Parser [+T] extends (Input) ⇒ ParseResult[T]

看起来它是一个接受输入并将其映射到结果的函数。嗯,这是有道理的!您可以在此处查看它的文档。

现在我们可以查找~方法:

def ~ [U] (q: ⇒ Parser[U]): Parser[~[T, U]]
  A parser combinator for sequential composition
  p ~ q' succeeds if p' succeeds and q' succeeds on the input left over by p'.

所以,如果我们看到类似的东西

def seaFacts = "fish" ~ "swim"

发生的情况是,首先,"fish"没有该~方法,因此它被隐式转换为Parser[String]which does。然后该~方法需要一个类型的参数Parser[U],因此我们隐式转换"swim"Parser[String](即U== String)。现在我们有了可以匹配输入的东西,输入"fish"中剩下的任何东西都应该匹配"swim",如果两者都是这种情况,那么seaFacts它的匹配就会成功。

于 2011-07-25T16:03:12.850 回答
13

parser 上的~方法将两个解析器合二为一,依次应用两个原始解析器并返回两个结果。那可能只是(在Parser[T]

def ~[U](q: =>Parser[U]): Parser[(T,U)]. 

如果您从未组合过两个以上的解析器,那也没关系。但是,如果将其中三个 , , , 与返回类型 , , , 链接p1起来p2p3T1,T2T3意味着p1 ~ p2 ~ p3p1.~(p2).~(p3)type Parser[((T1, T2), T3)]。如果你像你的例子中那样组合其中的五个,那就是Parser[((((T1, T2), T3), T4), T5)]. 然后,当您对结果进行模式匹配时,您也会拥有所有这些括号:

case ((((_, id), _), formals), _) => ...

这很不舒服。

然后是一个巧妙的句法技巧。当一个案例类有两个参数时,它可以出现在模式中的中缀而不是前缀位置。也就是说,如果您有 case class X(a: A, b: B),您可以使用 进行模式匹配case X(a, b),也可以使用case a X b. (这是用模式x::xs匹配非空列表所做的,::是一个案例类)。当你写 casea ~ b ~ c时,它​​的意思是case ~(~(a,b), c), but 更令人愉快,也比case ((a,b), c)too 更令人愉快,这很难正确。

因此~Parser 中的方法返回 aParser[~[T,U]]而不是 a Parser[(T,U)],因此您可以轻松地对多个 ~ 的结果进行模式匹配。除此之外, 它们几乎是一样的~[T,U](T,U)尽可能同构。

为解析器中的组合方法和结果类型选择相同的名称,因为结果代码易于阅读。可以立即看到结果处理中的每个部分如何与语法规则的项目相关联。

parser1 ~ parser2 ~ parser3 ^^ {case part1 ~ part2 ~ part3 => ...}

选择 Tilda 是因为它的优先级(它紧密绑定)与解析器上的其他运算符配合得很好。

最后一点,辅助运算符~><~丢弃其中一个操作数的结果,通常是规则中不携带有用数据的常量部分。所以宁愿写

"class" ~> ID <~ ")" ~ formals <~ ")"

并在结果中仅获取 ID 和形式的值。

于 2011-07-25T16:27:34.240 回答
3

你应该检查Parsers.Parser。Scala 有时会定义具有相同名称的方法和案例类以帮助模式匹配等,如果您正在阅读 Scaladoc,这会有点令人困惑。

特别是,"class" ~ ID与 相同"class".~(ID)~是一种将解析器与另一个解析器顺序组合的方法。

定义了一个隐式转换,它会自动从一个值RegexParsers创建一个解析器。String因此,"class"自动成为Parser[String].

val ID = """[a-zA-Z]([a-zA-Z0-9]|_[a-zA-Z0-9])*"""r

RegexParsersRegex还定义了另一个从值自动创建解析器的隐式转换。所以,ID自动成为Parser[String]too的一个实例。

通过组合两个解析器,"class" ~ ID返回与文字“类”匹配的 a ,然后是顺序出现Parser[String]的正则表达式。ID还有其他方法,例如||||。有关更多信息,请阅读Scala 中的编程

于 2011-07-25T16:01:35.687 回答