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















 
 
 


 
 
 
 