Perl 6 Grammars

doc.perl6.org(http://doc.perl6.org/language/grammars)

Grammars - 一組具名 regexes 組成正式的 grammar

Grammars 是一個(gè)很強(qiáng)大的工具用于析構(gòu)文本并通常返回?cái)?shù)據(jù)結(jié)構(gòu)。

例如, Perl 6 是使用 Perl 6 風(fēng)格 grammar 解析并執(zhí)行的汁讼。

對普通 Perl 6 使用者更實(shí)用的一個(gè)例子是 JSON::Tiny模塊, 它能反序列化任何合法的 JSON 文件, 而反序列代碼只有不到 100 行, 還能擴(kuò)展。

Grammars 允許你把 regexes 組織到一塊兒, 就像類(class) 中組織方法那樣皇钞。

具名正則 (Named Regexes)

grammars 的主要組成部分是 regexes。 而 Perl 6 的 regexes語法不在該文檔的討論范圍, 具名正則(named regexes) 有它自己的特殊語法, 這跟子例程(subroutine) 的定義很像:

my regex number { \d+ [ \. \d+ ]?   }   # 普通 regex 中空格被忽略, [] 是非捕獲組

上面的代碼使用 my 關(guān)鍵字指定了本地作用域的 regex, 因?yàn)榫呙齽t(named regexes) 通常用在 grammars 里面松捉。

正則有名字了就方便我們在任何地方重用那個(gè)正則了:

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 里面, 引用一個(gè)具名正則的語法也很特殊, 就是給名字包裹上 < ><> 就像引號那樣, 當(dāng)用它引起某個(gè)具名正則后, 引用這個(gè) `` 就會把該具名正則插入(帶入)到整個(gè)正則之中, 就像字符串插值那樣:

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 在正則構(gòu)造 // 里使用
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 內(nèi)部聲明的
     token TOP {  \s* '+' \s*  \s* '=' \s*  }
}

# 等式解析
my $expr = EquationParse.parse("12.88 + 0.12 = 13.00");
say $expr;

聲明具名正則不是只有一個(gè) regex 聲明符, 實(shí)際上 , regex 聲明符用的最少, 大多數(shù)時(shí)候, 都是使用 tokenrule 聲明符隘世。token 和 rule 這兩個(gè)都是 ratcheing (棘輪)的, 這意味著如果匹配失敗, 那么匹配引擎就不會回并嘗試匹配了可柿。這通常會是你想要的, 但不適用于所有情況:

棘輪用于單向驅(qū)動, 防止逆轉(zhuǎn)。

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, .+ 得到了整個(gè)字符串但不回溯

tokenrule 的唯一區(qū)別就是 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;

創(chuàng)建 Grammar

當(dāng)使用 grammar 關(guān)鍵字而非 class 關(guān)鍵字聲明來聲明一個(gè)類時(shí), 會自動得到以 Grammar 的父類以舒。Grammars 應(yīng)該只用于解析文本; 如果你想提取復(fù)雜的數(shù)據(jù), 推薦 action object和 grammar 一塊使用趾痘。

Protoregexes

如果你有很多備選分支(alternations), 那么生成可讀性好的代碼或子類化(subclass)你的 grammar 可能會變得很困難慢哈。在下面的 Actions 類中, TOP 方法中的三元操作符不是很完美并且當(dāng)我們添加更多的運(yùn)算符時(shí)它會變得更糟糕:

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<...> 那樣的副詞來使用正則表達(dá)式原型(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

在這個(gè) grammar 中, 備選分支(alternation)已經(jīng)被 <calc-op> 替換掉了, 它實(shí)質(zhì)上是我們將要創(chuàng)建的一組值的名字蔓钟。我們通過使用 proto rule calc-op 定義了一個(gè) rule 原型類型(prototype) 來達(dá)成。我們之前的每一個(gè)備選分支已經(jīng)被新的 rule calc-op 替換掉了并且備選分支的名字被附加上了 :sym<> 副詞卵贱。

在 actions 類中, 我們現(xiàn)在擺脫了三目操作符, 僅僅只在 $<calc-op> 匹配對象上接收 .made 值滥沫。并且單獨(dú)備選分支的 actions 現(xiàn)在和 grammar 遵守相同的命名模式: method calc-op:sym<add>method calc-op:sym<sub>

當(dāng)你子類化(subclass)那個(gè) grammar 和 actions 類的時(shí)候才能看到這個(gè)方法的真正魅力键俱。假設(shè)我們想為 calculator 增加一個(gè)乘法功能:

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, 感謝正則表達(dá)式原型(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 是默認(rèn)的第一個(gè)嘗試去匹配的 token , 當(dāng)解析一個(gè) grammar 的時(shí)候 - 那顆樹的根。注意如果你正使用 .parse 方法進(jìn)行解析, 那么 token TOP 被自動地錨定到字符串的開頭和結(jié)尾(再看看 .subparse)编振。

使用 rule TOPregex TOP 也是可以接受的缀辩。

.parse.subparse.parsefile Grammar 方法中使用 :rule 命名參數(shù)可以選擇一個(gè)不同的 token 來進(jìn)行首次匹配踪央。

ws

當(dāng)使用 rule 而非 token 時(shí), 原子(atom)后面的任何空白(whitespace)被轉(zhuǎn)換為一個(gè)對 ws 的非捕獲調(diào)用臀玄。即:

rule entry { <key> ’=’ <value> }

等價(jià)于:

token entry { <key> <.ws> ’=’ <.ws> <value> <.ws> } # . = non-capturing

默認(rèn)的 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(總是匹配成功). 當(dāng)它用作 grammar 中的 token 時(shí), 它可以被用于觸發(fā)一個(gè) Action 類方法累贤。在下面的 grammar 中, 我們查找阿拉伯?dāng)?shù)字并且使用 always succeed assertion 定義一個(gè) succ token叠穆。

在 action 類中, 我們使用對 succ 方法的調(diào)用來設(shè)置(在這個(gè)例子中, 我們在 @!numbers 中準(zhǔn)備了一個(gè)新元素)。在 digit 方法中, 我們把阿拉伯?dāng)?shù)字轉(zhuǎn)換為梵文數(shù)字并且把它添加到 @!numbers 數(shù)組的最后一個(gè)元素中臼膏。多虧了 succ, 最后一個(gè)元素總是當(dāng)前正被解析的 digit 數(shù)字的數(shù)硼被。

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 代替 ruletoken 也是可以的, 只要它們返回一個(gè) 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 會根據(jù) parse 方法提供的參數(shù)嘗試不同的匹配:

say +DigitMatcher.subparse: '12??????', args => \(:full-unicode);
# OUTPUT:
# 12717909

say +DigitMatcher.subparse: '12??????', args => \(:!full-unicode);
# OUTPUT:
# 12

Action Object

一個(gè)成功的 grammar 匹配會給你一棵匹配對象(Match objects)的解析樹, 匹配樹(match tree)到達(dá)的越深, 則 grammar 中的分支越多, 那么在匹配樹中航行以獲取你真正感興趣的東西就變的越來越困難。

為了避免你在匹配樹(match tree)中迷失, 你可以提供一個(gè) action object渗磅。grammar 中每次解析成功一個(gè)具名規(guī)則(named rule)之后, 它就會嘗試調(diào)用一個(gè)和該 grammar rule 同名的方法, 并傳遞給這個(gè)方法一個(gè)Match 對象作為位置參數(shù)祷嘶。如果不存在這樣的同名方法, 就跳過。

這兒有一個(gè)例子來說明 grammar 和 action:

use v6;

grammar TestGrammar {
    token TOP { ^ \d+ $ }
}

class TestActions {
    method TOP($/) {
        $/.make(2 + $/);  # 等價(jià)于 $/.make: 2 + $/
    }
}
my $actions = TestActions.new; # 創(chuàng)建 Action 實(shí)例
my $match   = TestGrammar.parse('40', :$actions);
say $match;       # ?40?
say $match.made;  # 42

TestActions 的一個(gè)實(shí)例變量作為具名參數(shù) actions 被傳遞給 parse 調(diào)用, 然后當(dāng) token TOP 匹配成功之后, 就會自動調(diào)用方法 TOP, 并傳遞匹配對象(match object) 作為方法的參數(shù)夺溢。

為了讓參數(shù)是匹配對象更清楚, 上面的例子使用 $/ 作為 action 方法的參數(shù)名, 盡管那僅僅是一個(gè)方便的約定, 跟內(nèi)在無關(guān)论巍。 $match 也可以。(盡管使用 $/可以提供把 $作為$/的縮寫的優(yōu)勢风响。)

下面是一個(gè)更有說服力的例子:

use v6;

grammar KeyValuePairs {
    token TOP {
        [ \n+]*
    }
    token ws { \h* } # 重寫了關(guān)于"空白"的定義
    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 這個(gè) rule, 解析一對由等號分割的 pair, 并且給 identifier 這個(gè) token 各自起了別名嘉汰。對應(yīng)的 action 方法構(gòu)建了一個(gè) Pair 對象, 并使用子匹配對象(sub match objects)的 .made 屬性。這也暴露了一個(gè)事實(shí): submatches 的 action 方法在那些調(diào)用正則/外部正則之前就被調(diào)用状勤。所以 action 方法是按后續(xù)調(diào)用的鞋怀。

名為 TOP 的 action 方法僅僅把由 pair 這個(gè) rule 的多重匹配組成的所有對象收集到一塊, 然后以一個(gè)列表的方式返回。

注意 KeyValuePairsActions 是作為一個(gè)類型對象(type object)傳遞給方法 parse的, 這是因?yàn)?action 方法中沒有一個(gè)使用屬性(屬性只能通過實(shí)例來訪問)持搜。

其它情況下, action 方法可能會在屬性中保存狀態(tài)密似。 那么這當(dāng)然需要你傳遞一個(gè)實(shí)例給 parse 方法。

注意, token ws 有點(diǎn)特殊: 當(dāng) :sigspace 開啟的時(shí)候(就是我們使用 rule的時(shí)候), 我們覆寫的 ws 會替換某些空白序列葫盼。這就是為什么 rule pair 中等號兩邊的空格解析沒有問題并且閉合 } 之前的空白不會狼吞虎咽地吃下?lián)Q行符, 因?yàn)閾Q行符在 TOP token 已經(jīng)占位置了, 并且 token 不會回溯残腌。

# ws 的內(nèi)置定義
/ <.ws> /                # match "whitespace":
                         #   \s+ if it's between two \w characters,
                         #   \s* otherwise
          
> my token ws { \h* } # 重寫 ws 這個(gè)內(nèi)置的 token
> say so "\n" ~~ &ws # True

所以 <.ws> 內(nèi)置的定義是:如果空白在兩個(gè) \w 單詞字符之間, 則意思為 \s+, 否則為 \s*。 我們可以重寫 ws 關(guān)于空白的定義, 重新定義我們需要的空白贫导。比如把 ws 定義為 { \h* } 就是所有水平空白符, 甚至可以將ws 定義為非空白字符抛猫。例如: token ws { 'x' }

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市孩灯,隨后出現(xiàn)的幾起案子闺金,更是在濱河造成了極大的恐慌,老刑警劉巖峰档,帶你破解...
    沈念sama閱讀 222,590評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件败匹,死亡現(xiàn)場離奇詭異,居然都是意外死亡讥巡,警方通過查閱死者的電腦和手機(jī)掀亩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來尚卫,“玉大人归榕,你說我怎么就攤上這事≈ㄉ妫” “怎么了刹泄?”我有些...
    開封第一講書人閱讀 169,301評論 0 362
  • 文/不壞的土叔 我叫張陵外里,是天一觀的道長。 經(jīng)常有香客問我特石,道長盅蝗,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,078評論 1 300
  • 正文 為了忘掉前任姆蘸,我火速辦了婚禮墩莫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘逞敷。我一直安慰自己狂秦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,082評論 6 398
  • 文/花漫 我一把揭開白布推捐。 她就那樣靜靜地躺著裂问,像睡著了一般。 火紅的嫁衣襯著肌膚如雪牛柒。 梳的紋絲不亂的頭發(fā)上堪簿,一...
    開封第一講書人閱讀 52,682評論 1 312
  • 那天,我揣著相機(jī)與錄音皮壁,去河邊找鬼椭更。 笑死,一個(gè)胖子當(dāng)著我的面吹牛蛾魄,可吹牛的內(nèi)容都是我干的虑瀑。 我是一名探鬼主播,決...
    沈念sama閱讀 41,155評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼畏腕,長吁一口氣:“原來是場噩夢啊……” “哼缴川!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起描馅,我...
    開封第一講書人閱讀 40,098評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎而线,沒想到半個(gè)月后铭污,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,638評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡膀篮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,701評論 3 342
  • 正文 我和宋清朗相戀三年嘹狞,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片誓竿。...
    茶點(diǎn)故事閱讀 40,852評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡磅网,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出筷屡,到底是詐尸還是另有隱情涧偷,我是刑警寧澤簸喂,帶...
    沈念sama閱讀 36,520評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站燎潮,受9級特大地震影響喻鳄,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜确封,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,181評論 3 335
  • 文/蒙蒙 一除呵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧爪喘,春花似錦颜曾、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,674評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至秃症,卻和暖如春候址,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背种柑。 一陣腳步聲響...
    開封第一講書人閱讀 33,788評論 1 274
  • 我被黑心中介騙來泰國打工岗仑, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人聚请。 一個(gè)月前我還...
    沈念sama閱讀 49,279評論 3 379
  • 正文 我出身青樓荠雕,卻偏偏與公主長得像,于是被迫代替她去往敵國和親驶赏。 傳聞我的和親對象是個(gè)殘疾皇子炸卑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,851評論 2 361

推薦閱讀更多精彩內(nèi)容