領域特定語言(DSL)近年來成為熱門話題,許多函數式語言受青睞正是因為它們擅長構建 DSL。在「面向 Java 開發人員的 Scala 指南」系列第八篇中,Ted Neward 將示範如何利用 Scala 的函數式特性構建一個簡單的計算器 DSL,重點介紹 case class 與模式匹配這兩大核心機制。
DSL 的本質是將應用程式的功能交到使用者手中。透過定義一個使用者能理解、直接操作的文字語言,開發者可擺脫不斷處理 UI 需求變更的困擾,同時賦予使用者編寫腳本、擴展應用行為的能力。最成功的 DSL 範例莫過於 Excel 的公式語言,甚至 SQL 也可視為針對關聯式資料庫的 DSL。
目標是讓用戶端程式碼能像這樣運作:
// Java 端呼叫
String s = "((5 * 10) + 7)";
double result = com.tedneward.calcdsl.Calculator.evaluate(s);
System.out.println("We got " + result); // 輸出 57
為了擴展性與封裝性,不採用簡單的字串解析器,而是遵循編譯器經典的兩階段架構:
case class 是 Scala 提供的便利機制,編譯器自動產生建構子、equals()、hashCode()、toString() 等方法,並支援模式匹配。例如:
case class Person(first: String, last: String, age: Int)
建立實例時無需 new:val ted = Person("Ted", "Neward", 37)。更重要的是,case class 採用結構相等性(bitwise equality),兩個欄位相同的實例視為相等。
模式匹配可解構物件、綁定變數、加入守衛條件(if),甚至匹配類型。結合 case class,可輕鬆處理樹狀結構:
p match {
case Person(_, _, a) if a > 30 => "they're certainly old."
case Person(_, "Neward", _) => "they come from good genes...."
case Person(first, last, ageInYears) if ageInYears > 17 =>
first + " " + last + " is " + ageInYears + " years old."
case _ => "I have no idea what to do with this person"
}
數學表達式遞迴結構天然對應樹狀 AST:
package com.tedneward.calcdsl {
private[calcdsl] abstract class Expr
private[calcdsl] case class Number(value: Double) extends Expr
private[calcdsl] case class UnaryOp(operator: String, arg: Expr) extends Expr
private[calcdsl] case class BinaryOp(operator: String, left: Expr, right: Expr) extends Expr
}
四行程式碼即完成任意深度算術表達式的型別階層。private[calcdsl] 限制存取範圍,隱藏實作細節。
object Calc {
def evaluate(e: Expr): Double = e match {
case Number(x) => x
case UnaryOp("-", x) => -evaluate(x)
case BinaryOp("+", x1, x2) => evaluate(x1) + evaluate(x2)
case BinaryOp("-", x1, x2) => evaluate(x1) - evaluate(x2)
case BinaryOp("*", x1, x2) => evaluate(x1) * evaluate(x2)
case BinaryOp("/", x1, x2) => evaluate(x1) / evaluate(x2)
}
}
每個分支回傳 Double,運算元先遞迴求值再執行運算。雖然違反傳統 OO 封裝觀念,但 AST 於包外不可見,客戶端僅傳入字串,這種「函數式風格」在封裝邊界內完全合理。
利用模式匹配的常數匹配與變數綁定,可在求值前進行代數簡化:
def simplify(e: Expr): Expr = e match {
case UnaryOp("-", UnaryOp("-", x)) => x
case UnaryOp("+", x) => x
case BinaryOp("*", x, Number(1)) => x
case BinaryOp("*", Number(1), x) => x
case BinaryOp("*", x, Number(0)) => Number(0)
case BinaryOp("*", Number(0), x) => Number(0)
case BinaryOp("/", x, Number(1)) => x
case BinaryOp("+", x, Number(0)) => x
case BinaryOp("+", Number(0), x) => x
case _ => e
}
求值器只需在匹配前呼叫 simplify(e) 即可。更進階的遞迴簡化(如巢狀結構的連鎖簡化)留待下一篇探討。
本文展示了 case class 與模式匹配如何協作,輕鬆構建並操作 AST。這是 Scala 乃至函數式程式設計的核心模式,也是構建外部 DSL 的強大基礎。下一篇將完成解析器,實現從字串到 AST 的轉換。