doc.perl6.org(http://doc.perl6.org/language/grammars)
Grammars - 一組具名 regexes 組成正式的 grammar
Grammars 是一個很強大的工具用于析構文本并通常返回數據結構。
例如, Perl 6 是使用 Perl 6 風格 grammar 解析并執行的。
對普通 Perl 6 使用者更實用的一個例子是 JSON::Tiny模塊, 它能反序列化任何合法的 JSON 文件, 而反序列代碼只有不到 100 行, 還能擴展。
Grammars 允許你把 regexes 組織到一塊兒, 就像類(class) 中組織方法那樣。
具名正則 (Named Regexes)
grammars 的主要組成部分是 regexes。 而 Perl 6 的 regexes語法不在該文檔的討論范圍, 具名正則(named regexes) 有它自己的特殊語法, 這跟子例程(subroutine) 的定義很像:
my regex number { \d+ [ \. \d+ ]? } # 普通 regex 中空格被忽略, [] 是非捕獲組
上面的代碼使用 my
關鍵字指定了本地作用域的 regex, 因為具名正則(named regexes) 通常用在 grammars 里面。
正則有名字了就方便我們在任何地方重用那個正則了:
say "32.51" ~~ &number;
say "15 + 4.5" ~~ / \s* '+' \s* /
&number # my regex number { \d+ [ \. \d+ ]? }
為什么用 &number
, 對比具名子例程你就知道了:
> sub number { say "i am a subroutine" } # 具名子例程
> &number # sub number () { #`(Sub|140651249646256) ... }
&number
就是直接引用了具名的 regex 或 子例程。而在/ /
或 grammars 里面, 引用一個具名正則的語法也很特殊, 就是給名字包裹上 < >
。<>
就像引號那樣, 當用它引起某個具名正則后, 引用這個 `` 就會把該具名正則插入(帶入)到整個正則之中, 就像字符串插值那樣:
use v6;
# 具名正則的聲明
my regex number { \d+ [ \. \d+]? }
my token ident { \w+ }
my rule alpha { <[A..Za..z]> }
# 1.0 通過 & 來引用
say so "12.34" ~~ &number; # true
# 2.0 在正則構造 // 里使用
say so "12.88 + 0.12" ~~ / \s* '+' \s* /; # true
# say so "12.88 + 0.12" ~~ / \s* '+' \s* /;
# wrong, method 'number' not found for invocant of class 'Cursor'
# 3.0 在 grammar 里面使用
grammar EquationParse {
# 這里也不能給 number 起別名, 除非 number 是在 grammar 內部聲明的
token TOP { \s* '+' \s* \s* '=' \s* }
}
# 等式解析
my $expr = EquationParse.parse("12.88 + 0.12 = 13.00");
say $expr;
聲明具名正則不是只有一個 regex
聲明符, 實際上 , regex 聲明符用的最少, 大多數時候, 都是使用 token
或 rule
聲明符。token 和 rule 這兩個都是 ratcheing
(棘輪)的, 這意味著如果匹配失敗, 那么匹配引擎就不會回并嘗試匹配了。這通常會是你想要的, 但不適用于所有情況:
棘輪用于單向驅動, 防止逆轉。
my regex works-but-slow { .+ q } # 可能會回溯
my token fails-but-fast { .+ q } # 不回溯
my $s = 'Tokens and rules won\'t backtrack, which makes them fail quicker!';
say so $s ~~ &works-but-slow; # True
say so $s ~~ &fails-but-fast; # False, .+ 得到了整個字符串但不回溯
token
和 rule
的唯一區別就是 rule
聲明符會讓正則中的 :sigspace
修飾符起效:
my token non-space-y { 'once' 'upon' 'a' 'time' }
my rule space-y { 'once' 'upon' 'a' 'time' }
say 'onceuponatime' ~~ &non-space-y;
say 'once upon a time' ~~ &space-y;
創建 Grammar
當使用 grammar 關鍵字而非 class 關鍵字聲明來聲明一個類時, 會自動得到以 Grammar 的父類。Grammars 應該只用于解析文本; 如果你想提取復雜的數據, 推薦 action object和 grammar 一塊使用。
Protoregexes
如果你有很多備選分支(alternations), 那么生成可讀性好的代碼或子類化(subclass)你的 grammar 可能會變得很困難。在下面的 Actions
類中, TOP
方法中的三元操作符不是很完美并且當我們添加更多的運算符時它會變得更糟糕:
grammar Calculator {
token TOP { [ <add> | <sub> ] }
rule add { <num> '+' <num> }
rule sub { <num> '-' <num> }
token num { \d+ }
}
class Calculations {
method TOP ($/) { make $<add> ?? $<add>.made !! $<sub>.made; }
method add ($/) { make [+] $<num>; }
method sub ($/) { make [-] $<num>; }
}
say Calculator.parse('2 + 3', actions => Calculations).made;
# OUTPUT:
# 5
為了讓世界變得更加美好, 我們可以在 tokens 身上使用看起來像 :sym<...>
那樣的副詞來使用正則表達式原型(protoregexes):
grammar Calculator {
token TOP { <calc-op> }
proto rule calc-op {*}
rule calc-op:sym<add> { <num> '+' <num> }
rule calc-op:sym<sub> { <num> '-' <num> }
token num { \d+ }
}
class Calculations {
method TOP ($/) { make $<calc-op>.made; }
method calc-op:sym<add> ($/) { make [+] $<num>; }
method calc-op:sym<sub> ($/) { make [-] $<num>; }
}
say Calculator.parse('2 + 3', actions => Calculations).made;
# OUTPUT:
# 5
在這個 grammar 中, 備選分支(alternation)已經被 <calc-op>
替換掉了, 它實質上是我們將要創建的一組值的名字。我們通過使用 proto rule calc-op
定義了一個 rule 原型類型(prototype) 來達成。我們之前的每一個備選分支已經被新的 rule calc-op
替換掉了并且備選分支的名字被附加上了 :sym<>
副詞。
在 actions 類中, 我們現在擺脫了三目操作符, 僅僅只在 $<calc-op>
匹配對象上接收 .made
值。并且單獨備選分支的 actions 現在和 grammar 遵守相同的命名模式: method calc-op:sym<add>
和 method calc-op:sym<sub>
。
當你子類化(subclass)那個 grammar 和 actions 類的時候才能看到這個方法的真正魅力。假設我們想為 calculator 增加一個乘法功能:
grammar BetterCalculator is Calculator {
rule calc-op:sym<mult> { <num> '*' <num> }
}
class BetterCalculations is Calculations {
method calc-op:sym<mult> ($/) { make [*] $<num> }
}
say BetterCalculator.parse('2 * 3', actions => BetterCalculations).made;
# OUTPUT:
# 6
所有我們需要添加的就是為 calc-op
組添加額外的 rule 和 action, 感謝正則表達式原型(protoregexes), 所有的東西都能正常工作。
特殊的 Tokens
TOP
grammar Foo {
token TOP { \d+ }
}
The TOP token is the default first token attempted to match when parsing with a grammar—the root of the tree. Note that if you're parsing with .parse method, token TOP is automatically anchored to the start and end of the string (see also: .subparse).
TOP
token 是默認的第一個嘗試去匹配的 token , 當解析一個 grammar 的時候 - 那顆樹的根。注意如果你正使用 .parse
方法進行解析, 那么 token TOP 被自動地錨定到字符串的開頭和結尾(再看看 .subparse
)。
使用 rule TOP
或 regex TOP
也是可以接受的。
在 .parse
、.subparse
或 .parsefile
Grammar 方法中使用 :rule
命名參數可以選擇一個不同的 token 來進行首次匹配。
ws
當使用 rule
而非 token
時, 原子(atom)后面的任何空白(whitespace)被轉換為一個對 ws
的非捕獲調用。即:
rule entry { <key> ’=’ <value> }
等價于:
token entry { <key> <.ws> ’=’ <.ws> <value> <.ws> } # . = non-capturing
默認的 ws
匹配"空白"(whitespace), 例如空格序列(不管什么類型)、換行符、unspaces、或 heredocs。
提供你自己的 ws
token 是極好的:
grammar Foo {
rule TOP { \d \d }
}.parse: "4 \n\n 5"; # Succeeds
grammar Bar {
rule TOP { \d \d }
token ws { \h* }
}.parse: "4 \n\n 5"; # Fails
上面的例子中, 在 Bar Gramamr 中重寫了自己的 ws
, 只匹配水平空白符, 所以 \n\n
匹配失敗。
總是成功斷言
<?>
is the always succeed assertion(總是匹配成功). 當它用作 grammar 中的 token 時, 它可以被用于觸發一個 Action 類方法。在下面的 grammar 中, 我們查找阿拉伯數字并且使用 always succeed assertion
定義一個 succ token。
在 action 類中, 我們使用對 succ 方法的調用來設置(在這個例子中, 我們在 @!numbers 中準備了一個新元素)。在 digit
方法中, 我們把阿拉伯數字轉換為梵文數字并且把它添加到 @!numbers 數組的最后一個元素中。多虧了 succ
, 最后一個元素總是當前正被解析的 digit
數字的數。
grammar Digifier {
rule TOP {
[ <.succ> <digit>+ ]+
}
token succ { <?> }
token digit { <[0..9]> }
}
class Devanagari {
has @!numbers;
method digit ($/) { @!numbers[*-1] ~= $/.ord.&[+](2358).chr }
method succ ($) { @!numbers.push: '' }
method TOP ($/) { make @!numbers[^(*-1)] }
}
say Digifier.parse('255 435 777', actions => Devanagari.new).made;
# OUTPUT:
# (??? ??? ???)
Grammar 中的方法
在 grammar 中使用 method
代替 rule
或 token
也是可以的, 只要它們返回一個 Cursor 類型:
grammar DigitMatcher {
method TOP (:$full-unicode) {
$full-unicode ?? self.num-full !! self.num-basic;
}
token num-full { \d+ }
token num-basic { <[0..9]>+ }
}
上面的 grammar 會根據 parse 方法提供的參數嘗試不同的匹配:
say +DigitMatcher.subparse: '12??????', args => \(:full-unicode);
# OUTPUT:
# 12717909
say +DigitMatcher.subparse: '12??????', args => \(:!full-unicode);
# OUTPUT:
# 12
Action Object
一個成功的 grammar 匹配會給你一棵匹配對象(Match objects)的解析樹, 匹配樹(match tree)到達的越深, 則 grammar 中的分支越多, 那么在匹配樹中航行以獲取你真正感興趣的東西就變的越來越困難。
為了避免你在匹配樹(match tree)中迷失, 你可以提供一個 action object。grammar 中每次解析成功一個具名規則(named rule)之后, 它就會嘗試調用一個和該 grammar rule 同名的方法, 并傳遞給這個方法一個Match
對象作為位置參數。如果不存在這樣的同名方法, 就跳過。
這兒有一個例子來說明 grammar 和 action:
use v6;
grammar TestGrammar {
token TOP { ^ \d+ $ }
}
class TestActions {
method TOP($/) {
$/.make(2 + $/); # 等價于 $/.make: 2 + $/
}
}
my $actions = TestActions.new; # 創建 Action 實例
my $match = TestGrammar.parse('40', :$actions);
say $match; # ?40?
say $match.made; # 42
TestActions
的一個實例變量作為具名參數 actions
被傳遞給 parse
調用, 然后當 token TOP
匹配成功之后, 就會自動調用方法 TOP
, 并傳遞匹配對象(match object) 作為方法的參數。
為了讓參數是匹配對象更清楚, 上面的例子使用 $/
作為 action 方法的參數名, 盡管那僅僅是一個方便的約定, 跟內在無關。 $match
也可以。(盡管使用 $/
可以提供把 $
作為$/
的縮寫的優勢。)
下面是一個更有說服力的例子:
use v6;
grammar KeyValuePairs {
token TOP {
[ \n+]*
}
token ws { \h* } # 重寫了關于"空白"的定義
rule pair {
'='
}
token identifier {
\w+
}
}
class KeyValuePairsActions {
method identifier($/) { $/.make: ~$/ }
method pair ($/) { $/.make: $.made => $.made }
method TOP ($/) { $/.make: $?.made }
}
my $res = KeyValuePairs.parse(q:to/EOI/, :actions(KeyValuePairsActions)).made;
second=b
hits=42
perl=6
EOI
for @$res -> $p {
say "Key: $p.key()\tValue: $p.value()";
}
這會輸出:
Key: second Value: b
Key: hits Value: 42
Key: perl Value: 6
pair
這個 rule, 解析一對由等號分割的 pair, 并且給 identifier
這個 token 各自起了別名。對應的 action 方法構建了一個 Pair
對象, 并使用子匹配對象(sub match objects)的 .made
屬性。這也暴露了一個事實: submatches 的 action 方法在那些調用正則/外部正則之前就被調用。所以 action 方法是按后續調用的。
名為 TOP
的 action 方法僅僅把由 pair
這個 rule 的多重匹配組成的所有對象收集到一塊, 然后以一個列表的方式返回。
注意 KeyValuePairsActions
是作為一個類型對象(type object)傳遞給方法 parse
的, 這是因為 action 方法中沒有一個使用屬性(屬性只能通過實例來訪問)。
其它情況下, action 方法可能會在屬性中保存狀態。 那么這當然需要你傳遞一個實例給 parse
方法。
注意, token ws
有點特殊: 當 :sigspace
開啟的時候(就是我們使用 rule
的時候), 我們覆寫的 ws
會替換某些空白序列。這就是為什么 rule pair
中等號兩邊的空格解析沒有問題并且閉合 }
之前的空白不會狼吞虎咽地吃下換行符, 因為換行符在 TOP
token 已經占位置了, 并且 token 不會回溯。
# ws 的內置定義
/ <.ws> / # match "whitespace":
# \s+ if it's between two \w characters,
# \s* otherwise
> my token ws { \h* } # 重寫 ws 這個內置的 token
> say so "\n" ~~ &ws # True
所以 <.ws>
內置的定義是:如果空白在兩個 \w
單詞字符之間, 則意思為 \s+
, 否則為 \s*
。 我們可以重寫 ws
關于空白的定義, 重新定義我們需要的空白。比如把 ws
定義為 { \h* }
就是所有水平空白符, 甚至可以將ws
定義為非空白字符。例如: token ws { 'x' }