Scala的模式匹配和條件類
本文源自Michel Schinz和Philipp Haller所寫的A Scala Tutorial for Java programmers,由Bearice成中文。第一篇為Scala簡單做了一下入門,第二篇描述Scala對象,第三篇對Scala類做了一些介紹。這一篇將介紹Scala中的一個重要特性:Scala的模式匹配。
51CTO編輯推薦:Scala編程語言專題
6 Scala的模式匹配和條件類
樹是在程序中常用的一個數據結構。例如編譯器和解析器常常吧程序表示為樹;XML文檔結構也是樹狀的;還有一些集合是基于樹的,例如紅黑樹。
接下來我們將通過一個計算器程序來研究樹在Scala中是如何表示和操縱的。這個程序的目標是處理一些由整數常量、變量和加號組成的簡單的算數表達式,例如1 + 2 和 (x + x ) + (7 + y )。
我們首先要決定如何表示這些表達式。最自然的方法就是樹了,樹的節點表示操作符(在這里只有加法),而樹的葉節點表示值(這里表示常數和變量)。 在Java中,這樣的樹可以表示為一個超類的樹的集合,節點由不同子類的實例表示。而在函數式語言中,我們可以使用代數類型(algebraic data-type)來達到同樣的目的。Scala提供了一種介于兩者之間的叫做條件類(Case Classes)的東西。
abstract class Tree
case class Sum(l: Tree, r: Tree) extends Tree
case class Var(n: String) extends Tree
case class Const(v: Int) extends Tree
我們實際上定義了三個條件類 Sum ,Var 和 Const 。這些類和普通類有若干不同:
- 實例化時可以省略new關鍵字(例如你可以使用 Const(5)而不必使用 new Const(5) )
- 參數的getter函數自動定義(例如你可以通過c.v來訪問類Const的實例c在實例化時獲取的參數v)
- 擁有默認的預定義equals和hashCode實現,這些實現可以按照值區別類實例是否相等,而不是通過用。
- 擁有默認的toString實現。這些實現返回值的代碼實現(例如表達式x+1可以被表達成Sum(Var(x),Const(1)))
- 條件類的實例可以通過模式匹配進行分析,我們接下來就要講這個特性。
現在我們已經定義了表示我們算數表達式的數據類型,于是我們可以開始給他們定義對應的操作。我們將會首先編寫一個在上下文中下計算表達式的函數。這里的上下文指的是變量與值的綁定關系。例如表達式x+1在x=5上下文中應該得出結果6。
這樣一來我們需要找到一個表示這種綁定關系的方法。當然我們可以使用某種類似hash-table的數據結構,不過我們也可以直接使用函數!一個上下文無非就是一個吧名稱映射到值的函數。例如上面給出的{x → 5}的這個映射我們就可以在Scala中表示為:
{ case "x" => 5 }
這個定義了一個函數:當參數等于字符串"x" 時返回整數5,否則拋出異常。
在編寫求值函數之前我們,我們需要給我們的上下文起個名字,以便在后面的代碼里面引用。理所應當的我們使用了類型String=>Int,但是如果我們給這個類型起個名字,將會讓程序更加簡單易讀,而且更加容易維護。在scala中,這件事情可以通過以下代碼完成:
type Environment = String => Int
從現在開始,類型Environment就當作String到Int的函數類型名來使用了。
現在我們可以開始定義求值函數了。從概念上來說,這是很簡單的一個過程:兩個表達式之和等于兩個表達式分別求值后再求和;變量的值可以從上下文中提取;常量的值就是他本身。在Scala中表達這個沒有什么難度:
def eval(t: Tree, env: Environment): Int = t match {
case Sum(l, r) => eval(l, env) + eval(r, env)
case Var(n) => env(n)
case Const(v) => v
}
求值函數通過對樹t進行模式匹配來完成工作。直觀的來看,上述代碼的思路是十分清晰的:
- 第一個模式檢查傳入的樹的根節點是否是一個Sum,如果是,它將會吧樹的左邊子樹賦值給l,右邊的子樹賦值給r,然后按照箭頭后面的代碼進行處理;這里的代碼可以(并且的確)使用了在左邊匹配時所綁定的變量,比如這里的l和r。
- 如果第一個檢查沒有成功,表明傳入的樹不是Sum,程序繼續檢查他是不是一個Var;如果是,則吧變量名賦給n然后繼續右邊的操作。
- 如果第二個檢查也失敗了,表示t既不是Sum也不是Var,程序檢查他是不是Const。如果是著賦值變量并且繼續。
- 最后,如果所有檢查都失敗了。就拋出一個異常表示模式匹配失敗。這只有在Tree的其他之類被定義時才可能發生。
我們可以看出模式匹配的基本思想就是試圖對一個值進行多種模式的匹配,并且在匹配的同時將匹配值拆分成若干子項,最后對匹配值與其子項執行某些代碼。
一個熟練的面向對象的程序員可能想知道為什么我們不吧eval定義為Tree或者其之類的成員函數。我們事實上可以這么做。因為Scala允許條件類象普通類那樣定義成員。決定是否使用模式匹配或者成員函數取決于程序員的喜好,不過這個取舍還和可擴展性有重要聯系:
- 當你使用成員函數時,你可以通過繼承Tree從而很容易的添加新的節點類型,但是另外一方面,添加新的操作也是很繁雜的工作,因為你不得不修改Tree的所有子類。
- 當你使用模式匹配是,形勢正好逆轉過來,添加新的節點類型要求你修改所有的對樹使用模式匹配的函數,但是另一方面,添加一個新的操作只需要再添加一個模式匹配函數就可以了。
下面我們來更詳細的了解模式匹配,讓我們再給表達式定義一個操作:對符號求導數。讀者們也許想先記住下面關于此操作的若干規則:
- 和的導數等于導數的和,
- 如果符號等以求導的符號,則導數為1,否則為0.
- 參數的導數永遠為0。
上述規則可以直接翻譯成Scala代碼:
def derive(t: Tree, v: String): Tree = t match {
case Sum(l, r) => Sum(derive(l, v), derive(r, v))
case Var(n) if (v == n) => Const(1)
case _ => Const(0)
}
這個函數使用了兩個關于模式匹配的功能,首先case語句可以擁有一個guard子句:一個if條件表達式。除非guard的條件成立,否則該模式不會成功匹配。其次是通配符:_ 。這個模式表示和所有值匹配而不對任何變量賦值。
事實上我們還遠沒有觸及模式匹配的全部精髓。但是我們限于篇幅原因不得不再此停筆了。下面我們看看這個兩個函數是如何在一個實例上運行的。為了達到這個目前我們寫了一個簡單的main函數來對表達式(x + x ) + (7 + y )進行若干操作:首先計算當{x → 5, y → 7}時表達式的值,然后分別對x和y求導。
def main(args: Array[String]) {
val exp: Tree = Sum(Sum(Var("x"),Var("x")),Sum(Const(7),Var("y")))
val env: Environment = { case "x" => 5 case "y" => 7 }
println("Expression: " + exp)
println("Evaluation with x=5, y=7: " + eval(exp, env))
println("Derivative relative to x:\n " + derive(exp, "x"))
println("Derivative relative to y:\n " + derive(exp, "y"))
}
執行程序,我們能得到以下輸出:
Expression: Sum(Sum(Var(x),Var(x)),Sum(Const(7),Var(y))) Evaluation with x=5, y=7: 24
Derivative relative to x: Sum(Sum(Const(1),Const(1)),Sum(Const(0),Const(0)))
Derivative relative to y: Sum(Sum(Const(0),Const(0)),Sum(Const(0),Const(1)))
通過研究程序輸出,我們能看到求導的輸出可以在被打印之前簡化,使用模式匹配定義一個簡化函數是挺有意思的(不過也需要一定的技巧)工作。讀者可以嘗試自己完成這個函數。
看到這里,希望大家對Scala的模式匹配有了一個大概的理解。下面一篇將介紹Scala Trait。
【相關閱讀】