|
| 1 | +回憶零.一版[實作遞迴下降](../零.一版/剖析(語法分析)#實作手寫遞迴下降)時,吾人曾歸納,遞迴下降函數中主要是兩種短路結構,一是「或」結構,一是「且」結構。 |
| 2 | + |
| 3 | +「或」結構對應語法的 `|` ,「且」結構對應語法的 `.`。重新看一次全是「且」結構的`變數宣告式`,其語法規則寫為: |
| 4 | + |
| 5 | +```語法 |
| 6 | +變數宣告式 = "元"・"・"・變數・"="・算式 |
| 7 | +``` |
| 8 | + |
| 9 | +其對應的遞迴下降函式: |
| 10 | + |
| 11 | +```rust |
| 12 | +fn 剖析變數宣告(&self, 游標: usize) -> Option<(O變數宣告, usize)> { |
| 13 | + let 游標 = self.消耗(游標, O詞::元)?; // 若匹配不了 "元" ,短路返回 None |
| 14 | + let 游標 = self.消耗(游標, O詞::音界)?; // 若匹配不了 "・" ,短路返回 None |
| 15 | + let (變數名, 游標) = self.剖析變數(游標)?; // 若匹配不了 變數 ,短路返回 None |
| 16 | + let 游標 = self.消耗(游標, O詞::等號)?; // 若匹配不了 "=" ,短路返回 None |
| 17 | + let (算式, 游標) = self.剖析算式(游標)?; // 若匹配不了 算式 ,短路返回 None |
| 18 | + |
| 19 | + Some((O變數宣告 { 算式, 變數名 }, 游標)) |
| 20 | +} |
| 21 | +``` |
| 22 | + |
| 23 | +可以看到遞迴下降函式相較語法規則要長上許多,有沒有辦法做簡化呢? |
| 24 | + |
| 25 | +前 5 行很相似,但在參數與回傳值中仍存在一些差異,如果改寫 `self.消耗(游標, O詞)` ,全改寫為 `self.消耗某詞(游標) -> ((), usize)`,前 5 行的形式就會完全相同: |
| 26 | + |
| 27 | +```rust |
| 28 | +fn 剖析變數宣告(&self, 游標: usize) -> Option<(O變數宣告, usize)> { |
| 29 | + let (_, 游標) = self.消耗元(游標)?; // 若匹配不了 "元" ,短路返回 None |
| 30 | + let (_, 游標) = self.消耗音界(游標)?; // 若匹配不了 "・" ,短路返回 None |
| 31 | + let (變數名, 游標) = self.剖析變數(游標)?; // 若匹配不了 變數 ,短路返回 None |
| 32 | + let (_, 游標) = self.消耗等號(游標)?; // 若匹配不了 "=" ,短路返回 None |
| 33 | + let (算式, 游標) = self.剖析算式(游標)?; // 若匹配不了 算式 ,短路返回 None |
| 34 | + |
| 35 | + Some((O變數宣告 { 算式, 變數名 }, 游標)) |
| 36 | +} |
| 37 | +``` |
| 38 | + |
| 39 | +前 5 行已經如此相似,寫出一個巨集`且!`(宏/macro)來生成類似代碼也許是個好主意: |
| 40 | +```rust |
| 41 | +fn 剖析變數宣告(&self, 游標: usize) -> Option<(O變數宣告, usize)> { |
| 42 | + let (_, _ , 變數名, _, _ 算式) = |
| 43 | + 且!(self.消耗元, self.消耗音界, self.消耗, self.消耗等號, self.剖析算式) |
| 44 | + Some((O變數宣告 { 算式, 變數名 }, 游標)) |
| 45 | +} |
| 46 | +``` |
| 47 | + |
| 48 | +至於這個`且!`具體要怎麽寫,就交由道友自行練習。除了採用宏來簡化,還有另一種函數式語言的思路。 |
| 49 | + |
| 50 | +## 組合式剖析器 |
| 51 | + |
| 52 | +如果仔細觀察遞迴下降函式的前 5 行,它們的共性是什麼?皆返回 `Option` ,且一遇失敗便回傳 `None` 結束函式,那能否透過 `Option` 內建的 `and_then` 函式來簡化程式呢? |
| 53 | + |
| 54 | + |
| 55 | +## 用 `and_then` 簡化「且」結構 |
| 56 | + |
| 57 | +```rust |
| 58 | +fn 剖析變數宣告(&self, 游標: usize) -> Option<(O變數宣告, usize)> { |
| 59 | + let (算式, 游標) = self |
| 60 | + .消耗元(游標) |
| 61 | + .and_then(|(_, 游標)| self.消耗音界(游標)) |
| 62 | + .and_then(|(_, 游標)| self.剖析變數(游標)) |
| 63 | + .and_then(|(變數名, 游標)| self.消耗賦值(游標)) |
| 64 | + .and_then(|(_, 游標)| self.剖析算式(游標))?; |
| 65 | + |
| 66 | + Some((O變數宣告 { 算式, 變數名 }, 游標)) |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +這寫法有一個大問題一個小問題。 |
| 71 | + |
| 72 | +- 大問題:`變數名`卡在閉包裡,外部截取不到,導致編譯失敗 |
| 73 | +- 小問題:`游標`還是反覆出現,改寫後程式碼簡潔不了多少 |
| 74 | + |
| 75 | +### 封裝 and_then |
| 76 | +先來解決小問題,在 `and_then` 方法上包裝一層就能消除掉重複使用 `游標` 了: |
| 77 | + |
| 78 | +```rust |
| 79 | +// Rust 得先宣告 trait 才能給內建型別加方法 |
| 80 | +trait 組合子<U> { |
| 81 | + fn 且<F>(self, f: F) -> Option<(U, usize)> |
| 82 | + where |
| 83 | + F: FnOnce(usize) -> Option<(U, usize)>; |
| 84 | +} |
| 85 | + |
| 86 | +impl<T, U> 組合子<U> for Option<(T, usize)> { |
| 87 | + fn 且<F>(self, f: F) -> Option<(U, usize)> |
| 88 | + where |
| 89 | + F: FnOnce(usize) -> Option<(U, usize)>, |
| 90 | + { |
| 91 | + // 其他行都在寫類型 |
| 92 | + // 可以先看這行就好:封裝掉游標傳遞 |
| 93 | + self.and_then(|(_語法, 游標)| f(游標)) |
| 94 | + } |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +在給 `Option<(T, usize)>` 加上`且`方法後就能將`剖析變數宣告`寫成 |
| 99 | +```rust |
| 100 | +fn 剖析變數宣告(&self, 游標: usize) -> Option<(O變數宣告, usize)> { |
| 101 | + let (算式, 游標) = self |
| 102 | + .消耗元(游標) |
| 103 | + .且(self.消耗音界) |
| 104 | + .且(self.剖析變數) |
| 105 | + .且(self.消耗賦值) |
| 106 | + .且(self.剖析算式)?; |
| 107 | + |
| 108 | + Some((O變數宣告 { 算式, 變數名 }, 游標)) |
| 109 | +} |
| 110 | +``` |
| 111 | +已經十分簡潔了。 |
| 112 | + |
| 113 | +實際上,上面這個函式編譯過不了,因為`消耗音界`的類型是`fn(&self: Self, 游標: usize)`,是一個接受兩個參數的函式,而 rust 不支援柯里化,編譯器接收到了一個 self 參數,仍會抱怨缺一參數。再進一步把每個剖析函數的回傳類型都改成 `(T, 游標, Self)` 或許能解決這問題。現在,暫時就先把參數加上去以過編譯。 |
| 114 | + |
| 115 | +傳遞 Self 也只是為了獲取 `VecDequeue<O詞>` 類型的 `詞流`,直接開個新結構把 `詞流` 以及 `游標` 的資訊一起傳遞還比較容易點,用切片又更簡單。 |
| 116 | + |
| 117 | +TODO: 剖析器零.一版範例程式一開始就用切片而非游標來當剖析函式的參數... |
| 118 | + |
| 119 | +### 傳遞剖析結果 |
| 120 | +再來解決剖析結果卡在閉包的大問題,解法一樣是透過包裝`且`來完成,讓每個剖析函數的的回傳值都能一路傳遞下來: |
| 121 | + |
| 122 | +```rust |
| 123 | +trait 組合子<T, U> { |
| 124 | + fn 且<F>(self, f: F) -> Option<((T, U), usize)> |
| 125 | + where |
| 126 | + F: FnOnce(usize) -> Option<(U, usize)>; |
| 127 | +} |
| 128 | + |
| 129 | +impl<T, U> 組合子<T, U> for Option<(T, usize)> { |
| 130 | + fn 且<F>(self, f: F) -> Option<((T, U), usize)> |
| 131 | + where |
| 132 | + F: FnOnce(usize) -> Option<(U, usize)>, |
| 133 | + { |
| 134 | + // 將之前的剖析結果與新剖析結果以 tuple 包裝 |
| 135 | + self.and_then(|(語法, 游標)| f(游標).map(|(新語法, 游標)| ((語法, 新語法), 游標))) |
| 136 | + } |
| 137 | +} |
| 138 | +``` |
| 139 | +最後回傳結構是巢狀的元組: |
| 140 | +```rust |
| 141 | +fn 剖析變數宣告(&self, 游標: usize) -> Option<(O變數宣告, usize)> { |
| 142 | + let ((((_, 變數名), _), 算式), 游標) = self |
| 143 | + .消耗元(游標) |
| 144 | + .且(self.消耗音界) |
| 145 | + .且(self.剖析變數) |
| 146 | + .且(self.消耗賦值) |
| 147 | + .且(self.剖析算式)?; |
| 148 | + |
| 149 | + Some((O變數宣告 { 算式, 變數名 }, 游標)) |
| 150 | +} |
| 151 | +``` |
| 152 | + |
| 153 | +這種寫法就被稱為組合子剖析器(也稱 parser combinator、剖析器組合子)。組合子函式泛指 `and_then`, `or_else`, `map`, `filter` 這類型高階函式,所以鏈式應用高階函式來寫剖析器就被叫做組合子剖析器了。 |
| 154 | + |
| 155 | +組合子剖析器能接近宏的簡潔程度,又避免掉宏不易除錯的問題。近年似乎漸露頭角,有好些新興的剖析庫都以此風格寫就。 |
| 156 | + |
| 157 | +## 用 `or_else` 如法炮製「或」結構 |
| 158 | +類似地,「或」結構也能用高階函數來簡化,回憶`句`的定義與遞迴下降函式: |
| 159 | + |
| 160 | +```語法 |
| 161 | +句 = 變數宣告式 |
| 162 | + | 算式 |
| 163 | +``` |
| 164 | + |
| 165 | +```rust |
| 166 | +fn 剖析句(&self, 游標: usize) -> Option<(O句, usize)> { |
| 167 | + if let Some((變數宣告, 游標)) = self.剖析變數宣告(游標) { |
| 168 | + return Some((O句::變數宣告(變數宣告), 游標)); |
| 169 | + } |
| 170 | + if let Some((算式, 游標)) = self.剖析算式(游標) { |
| 171 | + return Some((O句::算式(算式), 游標)); |
| 172 | + } |
| 173 | + None |
| 174 | +} |
| 175 | +``` |
| 176 | + |
| 177 | +「且」結構還能靠 Rust 的 `?` 來簡寫,「或」結構就得自己手寫 `if ... return` 來短路,顯得更加冗長。 |
| 178 | + |
| 179 | +先試著用 `or_else` 來簡化短路 |
| 180 | + |
| 181 | +```rust |
| 182 | +fn 剖析句(&self, 游標: usize) -> Option<(O句, usize)> { |
| 183 | + self.剖析變數宣告(游標) |
| 184 | + .map(|(變數宣告, 游標)| (O句::變數宣告(變數宣告), 游標)) |
| 185 | + .or_else(|| { |
| 186 | + self.剖析算式(游標) |
| 187 | + .map(|(算式, 游標)| (O句::算式(算式), 游標)) |
| 188 | + }) |
| 189 | +} |
| 190 | +``` |
| 191 | +好像沒什麼改善空間了,「或」語句不需要傳遞之前的剖析結果,而 `map` 中的類型轉換函數很難再省掉,就算省,也是在自動類型轉換上下功夫,與組合子關係不大了。 |
| 192 | + |
| 193 | +## 「重複」結構呢? |
| 194 | + |
| 195 | +如 `音界咒 = 句.句*` 這種「重複」結構,也可以透過組合子來達成,在 `且` 之外封上一層迴圈就能做到吧,同樣留作道友習題。 |
| 196 | + |
| 197 | + |
| 198 | +## 總結 |
| 199 | +組合子剖析器無非就是種風格,若嚴格地只使用組合子,那結構會很單純,風格會很一致。但想要混用原本遞迴下降平鋪直敘的寫法,或是加上一些宏也沒什麼問題。例如要結合應用前一章所說的優先級決定算法,也許遞迴下降的寫法更好寫一些喔。 |
| 200 | + |
| 201 | +零.二版的新語法脫不出這幾項結構,就不再將新的剖析代碼貼出來了。 |
0 commit comments