Ward Cunningham 曾說:「乾淨的代碼清晰表達作者意圖,而優美的代碼看起來就像是專為解決該問題而存在。」本文將展示一個組合式解析器的設計與實現過程,最終代碼極具擴展性,宛如為解析特定語法而生。我們將以 H.248 協議為例,實戰演練該解析器的構建,讓你在領略代碼之美的同時,掌握函數式編程與 DSL 構建的核心思想。
編程語言不僅是指揮計算機的工具,更是組織思維、表達計算過程的框架。根據《計算機程序的構造和解釋》(SICP),任何強大的語言都通過三種機制實現「組合封閉性」——即複雜概念經組合後,仍可像原子一樣繼續被組合:
Java 等通用語言提供的三大機制屬於通用層面,解決特定領域問題時會存在「語義鴻溝」。程序員的進階任務,正是提升語言層次,構建針對特定領域的 DSL(領域特定語言),清晰表達領域概念與規則,將通用語言轉化為專用語言。本文即以解析器構造為例,構建一個可擴展的字符串解析 DSL,並展示函數式編程的優雅。
什麼是解析器?概念上它是判斷輸入字符串是否符合語法並提取語法單元。為構建 DSL,需精確定義其類型。在 Java 中用接口表示:
interface Parser {
Result parse(String target);
}
Result 封裝解析結果:recognized(已識別部分)、remaining(剩餘字符串)、succeeded(是否成功),並提供靜態工廠方法 succeed 與 fail。
DSL 僅有兩個原子解析器:
這兩個原子奠定了所有複雜解析器的基礎。
有了原子,便可定義組合子(Combinator),將解析器組合成更強大的解析器,並保持「組合封閉性」——組合結果仍是 Parser。
接收一個謂詞 Predicate 與一個解析器,僅當原解析器成功且識別結果滿足謂詞時才成功:
class SAT implements Parser {
private Predicate pre;
private Parser parser;
public SAT(Predicate p, Parser parser) { this.pre = p; this.parser = parser; }
public Result parse(String target) {
Result r = parser.parse(target);
return (r.is_succeeded() && pre.satisfy(r.get_recognized())) ? r : Result.fail();
}
}
例:Parser digit = new SAT(new IsDigit(), new Item()); 解析單個數字。
嘗試第一個解析器,失敗則嘗試第二個:
class OR implements Parser {
private Parser p1, p2;
public OR(Parser p1, Parser p2) { this.p1 = p1; this.p2 = p2; }
public Result parse(String target) {
Result r = p1.parse(target);
return r.is_succeeded() ? r : p2.parse(target);
}
}
可組合出 digit_or_alpha 等解析器。
依次應用兩個解析器,均成功則合併識別結果:
class SEQ implements Parser {
private Parser p1, p2;
public SEQ(Parser p1, Parser p2) { this.p1 = p1; this.p2 = p2; }
public Result parse(String target) {
Result r1 = p1.parse(target);
if (!r1.is_succeeded()) return Result.fail();
Result r2 = p2.parse(r1.get_remaining());
return r2.is_succeeded() ? Result.concat(r1, r2) : Result.fail();
}
}
Result.concat 將兩結果的 recognized 拼接,remaining 取第二個結果的剩餘。
接收解析器與最大次數 max,連續應用該解析器 1 到 max 次,首次失敗則整體失敗,否則合併所有成功識別結果:
class OneOrMany implements Parser {
private int max; private Parser parser;
public OneOrMany(int max, Parser parser) { this.max = max; this.parser = parser; }
public Result parse(String target) {
Result r = parser.parse(target);
return r.is_succeeded() ? parse2(r, 1) : Result.fail();
}
private Result parse2(Result pre, int count) {
if (count >= max) return pre;
Result r = parser.parse(pre.get_remaining());
return r.is_succeeded() ? parse2(Result.concat(pre, r), count + 1) : pre;
}
}
例:Parser one_to_ten_alpha = new OneOrMany(10, alpha); 識別 1~10 個字母。
若組合結果無法命名並再次參與組合,DSL 擴展力大打折扣。本 DSL 直接借用 Java 接口與變量賦值機制作為抽象手段:
Parser alpha = new SAT(new IsAlpha(), new Item());
Parser digit = new SAT(new IsDigit(), new Item());
Parser digit_or_alpha = new OR(digit, alpha);
Parser alpha_before_digit = new SEQ(alpha, digit);
命名後的解析器即可像原子一樣繼續組合,實現了強大的擴展性。
H.248 協議中 NAME 定義為:
NAME = ALPHA *63(ALPHA / DIGIT / "_")
ALPHA = %x41-5A / %x61-7A ; A-Z, a-z
DIGIT = %x30-39 ; 0-9
*63 表示 0 到 63 次重複,/ 表示選擇,() 表示分組。利用上述 DSL 可直接映射:
class H248Parsec {
public static Parser alpha() { return new SAT(new IsAlpha(), new Item()); }
public static Parser digit() { return new SAT(new IsDigit(), new Item()); }
public static Parser underline() { return new SAT(new IsUnderline(), new Item()); }
public static Parser digit_or_alpha_or_underline() {
return new OR(alpha(), new OR(digit(), underline()));
}
// *n 表示 0 到 n 次,等價於 OR(OneOrMany(n, p), Zero())
public static Parser zero_or_many(int max, Parser parser) {
return new OR(new OneOrMany(max, parser), new Zero());
}
public static Parser name() {
return new SEQ(alpha(), zero_or_many(64, digit_or_alpha_or_underline()));
}
}
代碼與 ABNF 文法幾乎一一對應,完美詮釋了「優美代碼專為解決問題而生」。
本文展示了從兩個原子出發,通過四大組合子(SAT、OR、SEQ、OneOrMany)構建功能完備的解析器 DSL,並利用 Java 原生抽象機制實現組合封閉性。最終以 H.248 NAME 解析器為例,驗證了 DSL 的表達力與擴展性。這不僅是解析技術的演示,更是函數式組合思想與 DSL 設計哲學的實踐——讓代碼成為問題領域的精確語言。