解析器组合子
Background
在构建一个自顶向下的语法分析器时,很多情况下需要处理复杂的模式匹配来完成解析。比如对于文法:
E ::= '[' A ']' | '(' B ')'
需要写出如下 OCaml 代码:
let parseE input =
match input with
| '[' :: rest ->
let (a, rest') = parseA rest in
(match rest' with
| ']' :: rest'' -> (E_A a, rest'')
| _ -> failwith "Expected ']'")
| '(' :: rest ->
let (b, rest') = parseB rest in
(match rest' with
| ')' :: rest'' -> (E_B b, rest'')
| _ -> failwith "Expected ')'")
或者更简单一些:
let parseE input =
if String.length input >= 2 then
match input.[0], input.[String.length input - 1] with
| '[', ']' -> E_A (parseA (String.sub input 1 (String.length input - 2)))
| '(', ')' -> E_B (parseB (String.sub input 1 (String.length input - 2)))
| _ -> failwith "Expected '[' or '('"
else
failwith "Input too short"
但是,对于包含更多的终结符的文法,这个模式匹配只会更加复杂,严重影响代码的可维护性。可以使用解析器组合子来处理这个问题。
Parser combinator
解析器组合子的核心思想是:利用小的解析器“组合”成大的解析器。
假设词法单元的类型已经被定义为 token
:
type token =
| Ident of string
| Number of int
| Bool of bool
| Bracket of char
| Symbol of string
那么给出下面的解析器的定义:
type ['a] parser = token list -> ('a * token list) option
类型 parser
代表一类满足下面描述的函数:接受一个 token 列表,如果解析成功,返回一个解析结果和剩余的令牌列表,否则返回 None
为了接下来可以通过链式调用的方式把小的解析器组合起来,使用 OCaml 的 object 来包装一下 parser
:
class ['a] parser_obj (p : 'a parser) = object (self)
method parse = p
end
可以定义一个函数 ptoken
,函数接受一个函数 predicate
作为函数参数,返回一个解析器:如果 token 列表里面的第一个 token 满足 predicate
(也就是 predicate
返回 true
),那么就认为解析成功
method ptoken predicate =
let token_parser input =
match input with
| hd::tl when predicate hd -> Some (hd, tl)
| _ -> None
in
new parser_obj token_parser
还可以定义组合子 map
,它的作用是对解析器的结果进行变换。比如说,如果你有一个解析器能解析出一个 token,你可以用 map
把它变成你想要的任何类型(通常表现为丢掉某些解析结果或者把 token 变成字面量)。'b
是要转化的目标类型,'b.
表示该方法对任意类型 'b
都适用,('a -> 'b)
表示给出的转化函数 f
的类型,最后返回一个 'b parser_obj
method map : 'b. ('a -> 'b) -> 'b parser_obj =
fun f ->
let mapped_parser input =
match self#parse input with
| Some (a, rest) -> Some (f a, rest)
| None -> None
in
new parser_obj mapped_parser
combine
组合子的作用是把两个解析器顺序组合起来,先用第一个解析器解析输入,如果成功,再用第二个解析器解析剩下的输入。最终返回两个解析结果的元组:
method combine : 'b. 'b parser_obj -> ('a * 'b) parser_obj =
fun (other : 'b parser_obj) ->
let combined_parser input =
match self#parse input with
| Some (a, rest1) -> (
match other#parse rest1 with
| Some (b, rest2) -> Some ((a, b), rest2)
| None -> None
)
| None -> None
in
new parser_obj combined_parser
如果你想要“尝试”多个解析器,只要有一个成功就算成功,可以用 either 组合子:
method either : 'a parser_obj -> 'a parser_obj =
fun (other : 'a parser_obj) ->
let either_parser input =
match self#parse input with
| Some _ as res -> res
| None -> other#parse input
in
new parser_obj either_parser
最后,many 组合子重复使用同一个解析器,直到不能再解析为止,返回所有解析结果的列表:
method many : 'a list parser_obj =
let rec many_parser input =
match self#parse input with
| Some (a, rest) -> (
match many_parser rest with
| Some (lst, rest') -> Some (a :: lst, rest')
| None -> Some ([a], rest)
)
| None -> Some ([], input)
in
new parser_obj many_parser
Usage
比如对于文法
E = E + T | T
T = T * F | F
F = ( E ) | number
type expr =
| Add of expr * expr
| Mul of expr * expr
| Num of int
| Paren of expr
let parser = new parser_obj
let pnumber =
parser#ptoken (function Number _ -> true | _ -> false)
#map (function Number n -> n | _ -> failwith "not a number")
#map (fun n -> Num n)
let lparen = parser#ptoken (function Bracket '(' -> true | _ -> false)
let rparen = parser#ptoken (function Bracket ')' -> true | _ -> false)
let plus = parser#ptoken (function Symbol "+" -> true | _ -> false)
let star = parser#ptoken (function Symbol "*" -> true | _ -> false)
let rec pE tokens = pE_obj#parse tokens
and pT tokens = pT_obj#parse tokens
and pF tokens = pF_obj#parse tokens
and pE_obj =
let rec_e_plus_t =
(new parser_obj pE)
#combine plus
#combine (new parser_obj pT)
#map (fun ((e, _), t) -> Add (e, t))
in
rec_e_plus_t#either (new parser_obj pT)
and pT_obj =
let rec_t_mul_f =
(new parser_obj pT)
#combine star
#combine (new parser_obj pF)
#map (fun ((t, _), f) -> Mul (t, f))
in
rec_t_mul_f#either (new parser_obj pF)
and pF_obj =
(lparen#combine (new parser_obj pE)#combine rparen
#map (fun ((_, e), _) -> Paren e))
#either pnumber
let parse_expr tokens = pE_obj#parse tokens