语法是一个强大的工具,用于解构文本,通常用于返回通过解释该文本创建的数据结构。
例如,Raku 使用 Raku 风格的语法进行解析和执行。
对普通 Raku 用户来说更实用的例子是 JSON::Tiny 模块,它可以反序列化任何有效的 JSON 文件;但是,反序列化代码是用不到 100 行简单、可扩展的代码编写的。
如果你不喜欢学校里的语法,不要让它吓倒你。语法允许你对正则表达式进行分组,就像类允许你对常规代码的方法进行分组一样。
命名正则表达式§
语法的核心是命名正则表达式。虽然Raku 正则表达式的语法不在本文档的讨论范围之内,但命名正则表达式有特殊的语法,类似于子程序定义:[1]
my
在这种情况下,我们必须使用 my
关键字指定正则表达式是词法作用域的,因为命名正则表达式通常在语法中使用。
命名的好处是可以轻松地在其他地方重复使用正则表达式。
say so "32.51" ~~ ; # OUTPUT: «True»say so "15 + 4.5" ~~ /\s* '+' \s*/ # OUTPUT: «True»
regex
不是命名正则表达式的唯一声明符。事实上,它是最不常见的。大多数情况下,使用 token
或 rule
声明符。这两个都是棘轮,这意味着如果匹配引擎无法匹配任何内容,它不会回溯并重试。这通常会达到你想要的效果,但并不适用于所有情况。
mymymy = 'Tokens won\'t backtrack, which makes them fail quicker!';say so ~~ ; # OUTPUT: «True»say so ~~ ; # OUTPUT: «False»# the entire string is taken by the .+
请注意,非回溯作用于项,也就是说,就像下面的示例一样,如果你已经匹配了某些内容,那么你将永远不会回溯。但是,当你无法匹配时,如果 |
或 ||
引入了另一个候选者,你将再次尝试匹配。
my ;my ;say so "bd" ~~ ; # OUTPUT: «False»say so "bd" ~~ ; # OUTPUT: «True»
规则§
token
和 rule
声明符之间的唯一区别是,rule
声明符会导致 :sigspace
对正则表达式生效。
mymysay so 'onceuponatime' ~~ ; # OUTPUT: «True»say so 'once upon a time' ~~ ; # OUTPUT: «False»say so 'onceuponatime' ~~ ; # OUTPUT: «False»say so 'once upon a time' ~~ ; # OUTPUT: «True»
创建语法§
Grammar
是当类使用 grammar
关键字而不是 class
声明时,类自动获得的超类。语法应该只用于解析文本;如果你想提取复杂数据,可以在语法中添加操作,或者将操作对象与语法结合使用。
原型正则表达式§
Grammar
由规则、标记和正则表达式组成;这些实际上是方法,因为语法是类。
这些方法可以共享名称和功能,因此可以使用原型。
例如,如果你有很多交替,那么生成可读代码或对语法进行子类化可能会变得困难。在下面的 Calculations
类中,method TOP
中的三元运算符不太理想,而且随着我们添加的操作越多,它会变得更糟。
say Calculator.parse('2 + 3', actions => Calculations).made;# OUTPUT: «5»
为了改善这种情况,我们可以使用类似于 :sym<...>
副词的原型正则表达式。
say Calculator.parse('2 + 3', actions => Calculations).made;# OUTPUT: «5»
在这个语法中,交替现在已被 <calc-op>
替换,它本质上是我们将创建的一组值的名称。我们通过使用 proto rule calc-op
定义一个规则原型来做到这一点。我们之前的所有交替都被新的 rule calc-op
定义替换,并且交替的名称附加了 :sym<>
副词。
在声明操作的类中,我们现在去掉了三元运算符,只需从 $<calc-op>
匹配对象中获取 .made
值。并且各个交替的操作现在遵循与语法中相同的命名模式:method calc-op:sym<add>
和 method calc-op:sym<sub>
。
当对语法和操作类进行子类化时,这种方法的真正优势就体现出来了。假设我们想为计算器添加一个乘法功能。
is Calculatoris Calculationssay BetterCalculator.parse('2 * 3', actions => BetterCalculations).made;# OUTPUT: «6»
我们只需要在 calc-op
组中添加一个额外的规则和操作,它就可以工作——这一切都要归功于原型正则表达式。
特殊标记§
TOP
§
TOP
标记是使用语法解析时尝试匹配的默认第一个标记。请注意,如果你使用 .parse
方法进行解析,token TOP
会自动锚定到字符串的开头和结尾。如果你不想解析整个字符串,请查看 .subparse
。
使用 rule TOP
或 regex TOP
也是可以接受的。
可以使用:rule
命名参数指定第一个匹配的令牌,该参数可用于.parse
、.subparse
或 .parsefile
。这些都是 Grammar
方法。
ws
§
默认的 ws
匹配零个或多个空白字符,只要该点不在单词内(在代码形式中,即 token ws { <!ww> \s* }
)
# First <.ws> matches word boundary at the start of the line# and second <.ws> matches the whitespace between 'b' and 'c'say 'ab c' ~~ / ab c /; # OUTPUT: «「ab c」»# Failed match: there is neither any whitespace nor a word# boundary between 'a' and 'b'say 'ab' ~~ /. b/; # OUTPUT: «Nil»# Successful match: there is a word boundary between ')' and 'b'say ')b' ~~ /. b/; # OUTPUT: «「)b」»
请记住,我们在 ws
前面加了一个点以避免捕获,因为我们对此不感兴趣。由于一般来说空白字符是分隔符,所以它通常以这种方式出现。
当使用 rule
而不是 token
时,:sigspace
默认情况下处于启用状态,并且术语和闭合括号/方括号后的任何空白字符都将转换为对 ws
的非捕获调用,写为 <.ws>
,其中 .
表示非捕获。也就是说
与以下代码相同
您也可以重新定义默认的 ws
令牌
.parse: "4 \n\n 5"; # Succeeds.parse: "4 \n\n 5"; # Fails
甚至可以捕获它,但您需要显式使用它。请注意,在下一个示例中,我们使用 token
而不是 rule
,因为后者会导致空白字符被隐式非捕获的 .ws
占用。
;my = Foo.parse: "3 3";say <ws>; # OUTPUT: «「 」»
sym
§
<sym>
令牌可以在原型正则表达式中使用,以匹配该特定正则表达式的 :sym
副词的字符串值
.parse("I ♥ Raku", actions => class).made.say; # OUTPUT: «Raku»
当您已经使用要匹配的字符串来区分原型正则表达式时,这非常有用,因为使用 <sym>
令牌可以避免重复这些字符串。
"始终成功" 断言§
<?>
是始终成功断言。当用作语法令牌时,它可以用来触发 Action 类方法。在以下语法中,我们查找阿拉伯数字,并定义一个带有始终成功断言的 succ
令牌。
在动作类中,我们使用对 succ
方法的调用来进行设置(在本例中,我们准备 @!numbers
中的一个新元素)。在 digit
方法中,我们使用阿拉伯数字作为 Devanagari 数字列表的索引,并将其添加到 @!numbers
的最后一个元素中。由于 succ
,最后一个元素将始终是当前解析的 digit
数字的数字。
say Digifier.parse('255 435 777', actions => Devanagari.new).made;# OUTPUT: «(२५५ ४३५ ७७७)»
语法中的方法§
在语法中使用方法代替规则或令牌是可以的,只要它们返回 Match
上面的语法将尝试根据提供给 subparse 方法的参数进行不同的匹配
say +DigitMatcher.subparse: '12७१७९०९', args => \(:full-unicode);# OUTPUT: «12717909»say +DigitMatcher.subparse: '12७१७९०९', args => \(:!full-unicode);# OUTPUT: «12»
语法中的动态变量§
变量可以通过在定义它们的代码行前添加 :
来在令牌中定义。可以通过用花括号将任意代码嵌入令牌中的任何位置。这对于在令牌之间保持状态很有用,这可以用来改变语法解析文本的方式。在令牌中使用动态变量(带有 $*
、@*
、&*
、%*
twigils 的变量)会级联到定义它的令牌中定义的所有令牌,避免必须将它们作为参数从一个令牌传递到另一个令牌。
动态变量的一种用途是匹配的守卫。此示例使用守卫来解释哪些正则表达式类按字面意思解析空白字符
在这里,诸如“默认情况下使用规则来表示有意义的空白字符”之类的文本只有在规则、令牌或正则表达式是否被提及所分配的状态与正确的守卫匹配时才会匹配
say GrammarAdvice.subparse("use rules for significant whitespace by default");# OUTPUT: «use rules for significant whitespace by default»say GrammarAdvice.subparse("use tokens for insignificant whitespace by default");# OUTPUT: «use tokens for insignificant whitespace by default»say GrammarAdvice.subparse("use regexes for insignificant whitespace by default");# OUTPUT: «use regexes for insignificant whitespace by default»say GrammarAdvice.subparse("use regexes for significant whitespace by default");# OUTPUT: #<failed match>
语法中的属性§
可以在语法中定义属性。但是,只能通过方法访问它们。尝试从令牌内部使用它们将抛出异常,因为令牌是 Match
的方法,而不是语法本身的方法。请注意,从令牌中调用的方法内部修改属性将仅修改该令牌自身匹配对象的属性!如果语法属性被设置为公开,则可以在解析后返回的匹配中访问它们。
my = "\x[0d]\x[0a]";my = "GOT /index.html HTTP/1.1Host: docs.raku.orgbody";my = HTTPRequest.parse();say "type(\"$m.<type>\")=";# OUTPUT: type("GOT ")=Truesay "path(\"$m.<path>\")=";# OUTPUT: path("/index.html")=Falsesay "field(\"$m.<field>[0]\")=";# OUTPUT: field("Host: docs.raku.org")=False
注意:如果我们想以某种方式(在本不完整示例的上下文中)严格遵守 HTTP/1.1(RFC 2616),则需要 $crlf
和令牌 <.crlf>
。原因是 Raku 与 RFC 2616 不同,它符合 Unicode,并且 \r\n 需要被解释为单个 \n,从而阻止语法正确解析包含 \r\n 的字符串,这与 HTTP 协议的预期意义相符。请注意属性 invalid
如何针对每个组件是本地的(例如,<type>
的值为 True
,但 <path>
的值为 False
)。还要注意我们如何为 accept
提供了一个方法,原因是否则属性 invalid
将未初始化(即使存在)。
将参数传递到语法中§
要将参数传递到语法中,可以在语法任何解析方法的 :args
命名参数中使用它们。传递的参数应该在一个 list
中。
# Notice the comma after "sweets" when passed to :args to coerce it to a listsay demonstrate-arguments.parse("I like sweets", :args(("sweets",)));# OUTPUT: «「I like sweets」»
传递参数后,可以在语法内部对命名正则表达式进行调用时使用它们。
say demonstrate-arguments-again.parse("I like vegetables", :args(("vegetables",)));# OUTPUT: 「I like vegetables」»# OUTPUT: «phrase-stem => 「I like 」»# OUTPUT: «added-word => 「vegetables」»
或者,您可以初始化动态变量,并在语法内部以任何方式使用这些参数。
say demonstrate-arguments-dynamic.parse("I like everything else",:args(("everything", "else")));# OUTPUT: «「I like everything else」»# OUTPUT: «phrase-stem => 「I like 」»# OUTPUT: «added-words => 「everything else」»
动作对象§
成功的语法匹配会为您提供一个 Match
对象的解析树,并且匹配树越深,语法中的分支越多,导航匹配树以获取您真正感兴趣的信息就越困难。
为了避免需要深入匹配树,您可以提供一个动作对象。在语法中成功解析命名规则后,它会尝试调用与语法规则同名的一个方法,并将新创建的 Match
对象作为位置参数传递给它。如果不存在这样的方法,则会跳过它。
这是一个关于语法和动作实际应用的虚构示例
my = TestGrammar.parse('40', actions => TestActions.new);say ; # OUTPUT: «「40」»say .made; # OUTPUT: «42»
TestActions
的一个实例作为命名参数 actions
传递给 parse 调用,当令牌 TOP
成功匹配时,它会自动调用方法 TOP
,并将匹配对象作为参数传递。
为了明确参数是一个匹配对象,该示例使用 $/
作为动作方法的参数名称,但这只是一个方便的约定,没有本质上的区别;$match
也能工作,但使用 $/
确实提供了 $<capture>
作为 $/<capture>
的快捷方式的优势;无论如何,我们在 TOP
的动作中使用了另一个参数。
下面是一个更复杂的示例
my = KeyValuePairsActions;my = KeyValuePairs.parse(for ->
这将产生以下输出
Key: second Value: b Key: hits Value: 42 Key: raku Value: d
规则 pair
解析了由等号分隔的配对,将对令牌 identifier
的两个调用别名为单独的捕获名称,以便它们更容易更直观地使用,因为它们将在相应的动作中使用。相应的动作方法构造一个 Pair
对象,并使用子匹配对象的 .made
属性。因此它(与动作方法 TOP
一样)利用了子匹配的动作方法在调用/外部正则表达式之前调用的这一事实。因此,动作方法按 后序遍历 调用。
动作方法 TOP
只是收集了由 pair
规则的多个匹配 .made
的所有对象,并将它们返回到一个列表中。请注意,在这种情况下,我们需要使用 make
的方法形式,因为如果动作方法的参数是 $/
,则只能使用例程形式。反之,如果方法的参数是 $/
,我们可以简单地使用 make
,它等效于 $/.make
。
另请注意,KeyValuePairsActions
被作为类型对象传递给方法 parse
,这是因为所有操作方法都不使用属性(属性只能在实例中使用)。
我们可以通过使用继承来扩展上面的例子。
use KeyValuePairs;unit is KeyValuePairs;
我们正在对之前的例子进行子类化(实际上是子语法化);我们通过添加 comment
覆盖了 pair
的定义;之前的 TOP
规则已被降级为 configuration-element
,并且有一个新的 TOP
,它现在考虑由垂直空间分隔的配置元素集。我们还可以通过子类化操作类来重用操作。
use KeyValuePairs;unit is KeyValuePairsActions;method configuration-element()method TOP ()
所有现有的操作都被重用,尽管显然需要为语法中的新元素(包括 TOP
)编写新的操作。这些可以从这个脚本一起使用
use ConfigurationSets;use ConfigurationSetsActions;my = ConfigurationSetsActions;my = ConfigurationSets.parse(for @ ->
这将打印
Element→ second b hits 42 raku d Element→ third c hits 33
在其他情况下,操作方法可能希望在属性中保存状态。然后,您当然必须将实例传递给方法 parse。
请注意,token
ws
是特殊的:当 :sigspace
启用时(当我们使用 rule
时,它会启用),它会替换某些空格序列。这就是为什么 rule pair
中等号周围的空格可以正常工作,以及为什么 }
之前的空格不会吞噬 token TOP
中查找的换行符的原因。