此时对于我那幽默的《编译原理》课程还在用 C 语言写编译器的不满达到了顶峰。
你说得对,但是《编译原理》是由 Alfred V. Aho, Monica S. Lam, Ravi Sethi 和 Jeffrey D. Ullman 等人自主研发的一款全旧计算机世界编程游戏。游戏发生在一个被称作「C–」的简化语言,在这里,选择这门课的人将被迫享受「GCC 7.5.0」,没有一点 5202 年该有的体验。玩家将扮演一位名为「C 程序员」的神秘角色,在自由的编码中邂逅各种各样、时机诡异的 Segment Fault 们,和他们一起泄露内存,感受 C 语言的恶心之处——同时,逐步发掘「都 5202 年了这帮人还在拿 39 年前的书教文法分析」的真相。
但跑远了。总而言之这里介绍一种叫 parser combinator —— 解析器组合子的函数式手段,为您带来哪怕是手写递归下降也很舒适的体验(并非)
0. 介绍 ¶
Parser combinator(解析器组合子)是一种函数式编程中的技术,用于构建复杂语法分析器的工具。它的核心思想是:用小的、可组合的解析器函数构建更复杂的解析器。
一个 parser combinator 就是一个函数,它接受一些解析器作为输入,返回一个新的、更复杂的解析器。它是一种“组合模式”,非常适合处理递归文法。
一个经典的 Parser 可以是:
abbrev Parser (α : Type) := List Char → Option (α × List Char)
也就是说,一个 parser 就是是一个函数,接受 List Char 作为参数,如果解析成功,返回 .some (α × List Char)
为解析结果和剩余内容。如果失败,返回 .none
.
在这里,为了一步到位地解决报错,我们把返回类型改成 Except
abbrev Parser (α : Type) := List Char → Except String (α × List Char)
这样,如果报错就能看到一个字符串作为错误信息。
对于不熟悉函数式写法的人,这是它的类似的 Rust 代码(懒得配平生命周期了):
#![feature(type_alias_impl_trait)]
struct Parser<T>(Box<dyn FnOnce(&'static str) -> Result<(T, &'static str), String>>)
where
T: 'static;
impl<T> Parser<T> {
fn parse(self, input: &'static str) -> Result<(T, &'static str), String> {
self.0(input)
}
fn chain<U>(self, other: Parser<U>) -> Parser<(T, U)>
{
let f = self.0;
let g = other.0;
(move |input: &'static str| {
let (t, input) = f(input)?;
let (u, input) = g(input)?;
Ok(((t, u), input))
})
.into()
}
}
impl<T, F> From<F> for Parser<T>
where
F: FnOnce(&'static str) -> Result<(T, &'static str), String> + 'static
{
fn from(value: F) -> Self {
Parser(Box::new(value))
}
}
fn char(c: char) -> Parser<char> {
(move |input: &'static str| {
if input.is_empty() {
return Err("Input is empty".to_string());
}
if input.chars().next().unwrap() == c {
Ok((c, &input[c.len_utf8()..]))
} else {
Err(format!("Expected '{}', found '{}'", c, input.chars().next().unwrap()))
}
})
.into()
}
fn main() {
let res = char('a').parse("anc");
println!("Result: {:?}", res);
let res = char('a').chain(char('b')).parse("anc");
println!("Result: {:?}", res);
let res = char('a').chain(char('b')).parse("abc");
println!("Result: {:?}", res);
// Result: Ok(('a', "nc"))
// Result: Err("Expected 'b', found 'n'")
// Result: Ok((('a', 'b'), "c"))
}
其中,chain
就是一个 combinator,它用两个小的 parser 合成了一个大的 parser。
1. 抽象 ¶
让我们再次请出万能的 Monad 来。关于 Monad 是什么,不妨看看所以这个 Monads 到底是什么啊之让我学学
一个 parser 的合成过程经常需要前面的 parse 都 ok,一旦其中某一个不是 ok 的了,就短路返回 error。这是典型的 monad 行为
instance : Monad Parser where
pure := fun a =>
fun input => .ok (a, input)
bind := fun parser f =>
fun input => do
let (a, rest) ← parser input
f a rest
实现了 monad 的特征以后,我们就能方便的使用 do-记号,如
def symbol : Parser $ Int :=
fun input =>
match input with
| '+' :: xs => .ok (1, xs)
| '-' :: xs => .ok (-1, xs)
| xs => .ok (1, xs)
def digits : Parser $ List Char :=
fun input =>
.ok $ input.span (·.isDigit)
def parseInt : Parser Json := do
let symbol ← symbol
let numStr ← digits
match numStr.asString.toInt? with
| some n => return Json.int $ symbol * n
| none => fail "expected an integer, but got non-digit characters"
#eval parseInt "12345abced12345".data
-- Except.ok (Json.int 12345, ['a', 'b', 'c', 'e', 'd', '1', '2', '3', '4', '5'])
其次,一个 parser 还能实现 Alternative
(选择子)。字面意义上地,
An
Alternative
functor is anApplicative
functor that can “fail” or be “empty” and a binary operation<|>
that “collects values” or finds the “left-most success”.Important instances include
Option
, wherefailure := none
and<|>
returns the left-mostsome
.- Parser combinators typically provide an
Applicative
instance for error-handling and backtracking.Error recovery and state can interact subtly. For example, the implementation of
Alternative
forOptionT (StateT σ Id)
keeps modifications made to the state while recovering from failure, whileStateT σ (OptionT Id)
discards them.
关于函子 functor 的概念我推荐阅读图解 Functor、Applicative、Monad。这样,我们用
parser1 <|> parser2
就能描述“第一个解析成功的 parser”。
或许 parser 还适用一些别的抽象,但我也是刚学,想到写什么就放些什么了()总而言之,让我们又双叒叕开启愉快的 JSON 解析之旅。
2. 实践:JSON 解析器 ¶
还是熟悉的 JSON
inductive Json where
| null
| bool (x: Bool)
| int (x: Int)
| float(x: Float)
| str (x: String)
| arr (x: List Json)
| obj (x: List (String × Json))
deriving Repr
这次我们要正确地解析数字,所以引入了 int 和 float 类型。然后是准备工作:
abbrev Parser (α : Type) := List Char → Except String (α × List Char)
def fail (reason : String) : Parser α :=
fun _ => .error reason
-- 无论如何总是返回失败原因的函数
instance : Monad Parser where
pure := fun a =>
fun input => .ok (a, input)
bind := fun p f =>
fun input => do
let (a, rest) ← p input
f a rest
instance : Alternative Parser where
failure := fail "unknown error"
orElse p₁ p₂ := fun input =>
match p₁ input with
| .ok r => .ok r
| .error _ => p₂ () input
接下来可以实践写几个辅助函数
-- 返回一个拿一个 char 的 parser
def char (c : Char) : Parser Char :=
fun input =>
match input with
| x :: xs =>
if x == c then
.ok (c, xs)
else .error s!"Expected '{c}', got '{x}'"
| [] => .error s!"Unexpected EOF, expected character '{c}'"
-- 返回一个拿走前面的 whitespace 的 parser
def whitespaces : Parser Unit :=
fun input =>
.ok ((), input.dropWhile (·.isWhitespace))
-- 返回一个拿一个 + 或者 - 符号的 parser
def symbol : Parser $ Int :=
fun input =>
match input with
| '+' :: xs => .ok (1, xs)
| '-' :: xs => .ok (-1, xs)
| xs => .ok (1, xs)
-- 返回一个拿数字的 parser
def digits : Parser $ List Char :=
fun input =>
.ok $ input.span (·.isDigit)
这样, number 的 parse 工作就变得很简单,只需要分两种情况:
def parseInt : Parser Json := do
let symbol ← symbol
let numStr ← digits
match numStr.asString.toInt? with
| some n => return Json.int $ symbol * n
| none => fail "expected an integer, but got non-digit characters"
def toFloat (num : List Char) (fraction : List Char) : Option Float := do
let num ← num.asString.toNat?
let fra ← fraction.asString.toNat?
let exp := 0.1 ^ fraction.length.toFloat
return num.toFloat + (fra.toFloat * exp)
def parseFloat : Parser Json := do
let symbol ← symbol
let num ← digits
let _ ← char '.'
let fraction ← digits
match toFloat num fraction with
| .some num => return Json.float $ if symbol == -1 then -num else num
| .none => fail "expected a float"
def parseNumber : Parser Json := do
parseFloat <|> parseInt
现在我们的 paserNumber
就是一个既可以解析整数又可以解析浮点数的 parser 了!
随后我们一鼓作气,把几个简单的 parser 写掉。先写字符串:
def quotedString : Parser $ List Char := do
_ ← char '"'
fun input =>
let (str, last) := input.span (· != '"')
.ok (str, last.drop 1)
def parseString : Parser Json := do
let str ← quotedString
return (Json.str str.asString)
为什么要分 quoted string 和 string 呢?因为后者直接得到了一个 Json
,但我们还要在 object 那里复用获得字符串的 parser。
随后是几个关键字,异常简单:
def keyword (kw : String) (val : Json) : Parser Json :=
fun input =>
if input.asString.startsWith kw then
.ok (val, input.drop kw.length)
else .error s!"Expected keyword '{kw}', but got '{input.asString}'"
def parseTrue := keyword "true" $ Json.bool true
def parseFalse := keyword "false" $ Json.bool false
def parseNull := keyword "null" Json.null
现在是三个递归结构:
-- 这是一个辅助函数,用来把以 sep 分割的 a 生成一个 list parser
partial def sepBy (α : Parser a) (sep : Parser b) : Parser (List a) :=
fun input =>
let rec go (acc : List a) (input : List Char) : (List a × List Char) :=
match α input with
| .ok (a, rest) =>
match sep rest with
| .ok (_, rest') => go (acc.concat a) rest'
| .error _ => (acc.concat a, rest)
| .error _ => (acc, input)
.ok $ go [] input
mutual
partial def parseArray : Parser Json := do
_ ← char '['
_ ← whitespaces
let elems ← sepBy parseJson (char ',')
_ ← whitespaces
_ ← char ']'
return Json.arr elems
partial def parseObject : Parser Json := do
_ ← char '{'
_ ← whitespaces
let pairs ← sepBy parsePair (char ',')
_ ← whitespaces
_ ← char '}'
return Json.obj pairs
partial def parsePair : Parser (String × Json) := do
_ ← whitespaces
let key ← quotedString
_ ← whitespaces
_ ← char ':'
let value ← parseJson
return (key.asString, value)
partial def parseJson : Parser Json := do
let _ ← whitespaces
let json ← fun str => match str with
| 'n' :: _ => parseNull str
| 't' :: _ => parseTrue str
| 'f' :: _ => parseFalse str
| '[' :: _ => parseArray str
| '{' :: _ => parseObject str
| '"' :: _ => parseString str
| _ => parseNumber str
let _ ← whitespaces
return json
end
整个逻辑极其清晰和简单!这就是 parser combinator 的魅力。回顾我们之前写的 lean4 的 json 大概长这样
def parseJsonObject (fuel: Nat) (str: List Char) : Except String (List (String × Json) × List Char) := do
match fuel with
| 0 => .error "out of fuel"
| .succ fuel =>
let mut xs := trimList str
let mut ret : List (String × Json) := []
while true do
match xs with
| '"' :: next =>
let (key, next) ← parseJsonString next
xs := trimList next
if xs.head? != some ':' then
.error "expect `:` but got unexpected token"
else
xs := xs.tail
let (val, next) ← parseJsonValue fuel xs
ret := ret.append [(key, val)]
match trimList next with
| ',' :: next => xs := trimList next
| '}' :: next =>
return (ret, next)
| x => .error $ "expect `,` or `}` but got unexpected token:" ++ String.mk x
| '}' :: next =>
return (ret, next)
| x => .error $ "unexpected token:" ++ String.mk x
return ([], xs.tail)
termination_by fuel
虽然比起抽象更少的 gleam 还是很不错,但对比 parser combinator 的做法:
partial def parseObject : Parser Json := do
_ ← char '{'
_ ← whitespaces
let pairs ← sepBy parsePair (char ',')
_ ← whitespaces
_ ← char '}'
return Json.obj pairs
partial def parsePair : Parser (String × Json) := do
_ ← whitespaces
let key ← quotedString
_ ← whitespaces
_ ← char ':'
let value ← parseJson
return (key.asString, value)
显然,所有人都看得出来, parser combinator 的做法非常简洁明了,可读性极强。
如果你正在编写一个传统的递归下降解析器来解析 JSON 数组,你可能会尝试在循环中解析语句,当它无法解析中间的逗号时停止。这个过程很难做到复用,除非你其实已经在写一个 parser combinator 了。比如我之前写的 C++ 的解析器:
class Object : public Node {
using ObjectVT = std::unordered_map<std::string, JSON>;
ObjectVT value_{};
public:
Object() {};
explicit Object(ObjectVT &&val) : value_(std::move(val)) {};
Object(Object &&) = default;
Object(const Object &) = delete; // 删掉拷贝构造函数
Object &operator=(Object &&) = default;
Object &operator=(const Object &) = delete;
inline static std::unique_ptr<Object> parse(std::string_view &sv, int dep) {
assert_depth(sv, dep);
removeWhiteSpaces(sv);
if (sv[0] != '{')
throw getJSONParseError(sv, "object start `{`");
sv.remove_prefix(1);
removeWhiteSpaces(sv);
ObjectVT val;
bool isTComma = false; // 是不是尾随逗号
while (sv[0] != '}') {
auto key = String::parse(sv); // 解析 key
if (ENABLE_DUMPLICATED_KEY_DETECT && val.contains(key->value())) {
throw getJSONParseError(
sv, ("unique key, but got dumplicated key `" + key->value() + "`")
.c_str());
} else {
removeWhiteSpaces(sv);
if (sv[0] != ':') // 解析 key :
throw getJSONParseError(sv, "object spliter `:`");
sv.remove_prefix(1);
val.insert({key->take(), JSON(Node::parse(sv, dep + 1))});
removeWhiteSpaces(sv); // 解析 key : value
switch (sv[0]) {
case '}': // 看看结束了没
sv.remove_prefix(1);
return std::make_unique<Object>(std::move(val));
case ',':
sv.remove_prefix(1);
isTComma = true;
continue;
default:
throw getJSONParseError(sv, "object spliter `,` or object end `}`");
}
}
}
if (isTComma && !ENABLE_TRAILING_COMMA)
throw getJSONParseError(sv, "next json value");
sv.remove_prefix(1);
return std::make_unique<Object>(std::move(val));
}
// ...
}
但如你所见,使用函数式语言 parser combinator 的话,定义一个复用的解析器极其简单
def arrayElems := sepBy parseJson (char ',')
def urlParams := sepBy (agg3 $ parseVar (char '=') parseVal) (char '&')