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