Parser是根據一串文字的格式,將這串文字拆解成好幾個段落。為了符合
所處理的文字內容,通常會預設文字內容都會符合一套文法。而文法,
通常用BNF定義。在BNF中,我們會看到像 | 這個符號代表提供二種 parsing
rules 的選擇,另外有先parse某個符號然後忽略這個符號、之後要parse
譬如說一串浮點數這樣的文字內容。
Parser Combinators 是認為可以將幾個最基本的parsing運算定義為基本
單元,然後幾個parsing運算之間可以做類似shell command的pipe-line
串連。前文所提 Graham Hutton 的文章 "Higher Order Functions For Parsing"
即介紹這一種程式技巧。
用Erlang語言來想,假設有個parser可以解一段文字 "1+2-3" 此數學算式,
當parser成功解掉那段文字,我們可得到輸出值為:
[ {"(1+2)-3", []}, {"1+(2-3)", []} ]
然而若讓這個parser解 "1+2-" 這段文字,因為不符合文法,所以輸出值為:
[]
Hutton之文章是以函數語言為討論基礎,語言預設有 lazy evaluation 策略,
像Haskell語言那樣。但以Erlang語言來看,Erlang預設為 eager evaluation 策略,
意思是如果有以下的式子:
f(g(), h())
g, h二個函數一定先執行,然後執行 f 函數。不過雖然如此,仍可以用語言
技巧將 lazy evaluation 明確地指定出來。我認為有二種方式算是 lazy evaluation,
一種是把函數名稱當做參數使用:例如以下一行,
fun g/0
它只是一個函數、不接收任何參數,並且函數還沒執行,而是要等到你給足
參數之後才會執行。第二種方式是將函數多加套一層lambda expression格式:
fun() -> ... end
就讓各種函數變成 lazy :例如,本來有個函數求二數相加,
f(X, Y) -> X + Y.
改成 lazy 版本為
f(X, Y) ->
fun() -> X + Y end.
同樣呼叫 f(5, 3) 的時候,後一個版本變成是一個函數,要到呼叫寫成 (f(5,3))()
的時候才會計算 5 + 3 。
Lazy evaluation 對 parser combinators 很重要,因為很多文法定義都是遞迴定義。
如果走 eager evaluation 會把文法規則程式卡死。 Lazy evaluation 也是我自己用
Erlang實作這個時,時常卡到的問題。
====================================
那麼,可以開始介紹Parser Combinators的實作過程。
先定義parsing輸出資料為以下格式:
[ { parse出來的值, 剩下未parse的文字 }, ... ]
首先可以定義幾個基本parsing單元。 fun succeed/1 是無條件parsing成功,
fun fail/0 是無條件parsing無效, fun satisfy/1 是根據一個邏輯判斷決定是否
parsing成功。
succeed(V) ->
fun(Input) -> [{V, Input}] end.
fail() ->
fun(_Input) -> [] end.
satisfy(Predicate) ->
fun(Input) -> case Input of
[] -> (fail())();
[X|Xs] -> case Predicate(X) of
true -> (succeed(X))(Xs);
false -> (fail())([X|Xs])
end
end
end.
在此每個函數都先接收基本必要的參數,然後要等待一段文字當作 Input 參數。
我覺得 fun(Input) -> 就代表了有 lazy evaluation 效果的等待。
那就可以定義解一個字母的parser為 fun literal/1 :
literal(A) ->
satisfy(fun(X)-> X == A end).
這樣,像 (literal(a))([a,b,c]) 就可以解出為 [{a, [b,c]}] 。而假如解不出來,
像 (literal(a))([b,c,d]) ,輸出值則是 [] 。 Parsing 成功或失敗的定義很明確。
四則運算的BNF定義,開頭可能像
Expr ::= Factor '*' Expr | Factor '/' Expr | Term
是說算式可能是乘法式子或除法式子、或一個普通式子。這裏的「或」是一個
parser combinator ,定義為 fun alt/2 :
alt(Parser1, Parser2) ->
fun(Input) -> Parser1(Input) ++ Parser2(Input) end.
fun alt/2 是指二個parsers各自解同一段輸入文字,各自產生輸出值,二份輸出值
合起來算是整個完成的結果。
fun then/2 同樣拿二個 parsers ,先用一個解文字,解出來剩下的未parsing文字
再給另一個parser解。所以程式是:
then(Parser1, Parser2) ->
fun(Input) ->
[ { {V1, V2}, Out2 } || {V1, Out1} <- Parser1(Input),
{V2, Out2} <- Parser2(Out1) ]
end.
如果多次使用 fun then/2 ,看使用的結構如何,輸出值也符合相同結構。像是
(then(literal(a), then(literal(b), literal(c)))([a,b,c]) 產生 [{{a,{b,c}}, []}] ,而
(then(then(literal(a), literal(b)), literal(c)))([a,b,c]) 產生 [{{{a,b},c}, []}] 。輸出值
基本收在tuple中,而因為tuple是有限元素的ordered pair,特性不如 list 好用,
所以會有 fun using/2 和 fun cons/1 來幫忙讓 {V1, V2} 變成 [V1, V2] 。
using(Parser, Func) ->
fun(Input) ->
[ { Func(V), Out } || {V, Out} <- Parser(Input) ]
end.
cons({X,Y}) ->
if is_list(Y) -> [X|Y];
所以,像有些BNF要定義 S ::= T * 表示有連續0個或多個 T ,可以定義為
fun many/1 :
many(Parser) ->
fun(Input) ->
(alt(using(then(Parser, many(Parser)),
fun cons/1),
succeed([])))(Input)
end.
用起來像 (many(literal(a)))([a,a,b]) 會解出 [{[a,a], [b]}, {[a], [a, b]}, {[], [a,a,b]}]
這樣,把所有連續任意多個 a 的解出結果都拿出來。
在這裏提一下之前我卡到的 lazy evaluation 不成功的心得。我發現如果 fun many/1
寫成像以下錯誤的寫法:
many(Parser) ->
alt(using(then(Parser, many(Parser)),
fun cons/1),
succeed([])).
雖然按照函數語言的用法感覺很對,但是執行 many(literal(a)) 就啟動一個無窮
遞迴過程。 而寫成前面那個版本,因為多了 fun(Input) -> ...(Input) end ,讓它
遞迴呼叫多了一些舒緩 laziness 的過程,就可以執行了。我不知道要如何解釋
它那樣可以、這樣卻不行的具體理由。
接著以 fun some/1 做結尾。BNF會定義 T+ 代表至少有一個或者更多個 T 。
fun some/1 定義這個文法:
some(Parser) ->
fun(Input) ->
(using(then(Parser, many(Parser))
fun cons/1))(Input)
end.
Best Regards,
Y.-H. Huang