注:本文轉載自InfoQ – 王恆濤
Scala結合了物件導向程式設計與函式程式設計思想,使用一種能夠完全相容Java、可以執行在Java虛擬機器上的、簡潔的語法。對於函式程式設計風格的支援,尤其是對於Lambda表示式的支援,能夠有助於減少必須要編寫的邏輯無關固定程式碼,也許讓它可以更簡單的關注要面對的任務本身,而相對的Java中對Lamdba表示式的支援要到預定於2012年釋出的JavaSE8才會實現。本文就是對於Scala介紹。
作為第一步,先安裝好最新的Scala釋出包Typesafe stack,開啟命令列視窗,鍵入“scala”:這會啟動REPL(讀入-運算 輸出 迴圈)互動式編碼環境。然後你就可以寫下你的第一行Scala程式碼:
1 |
scala> val columbus : Int = 1492columbus: Int = 1492 |
我們剛剛宣告瞭一個型別為Int的變數,初始值為1492,就像我們在Java裡用語句Int columbus = 1492;所做的一樣。除了把型別放在變數之後這樣一種反向的宣告方式之外,Scala在這裡所表現出的不同是使用“val”來顯性地把變數宣告為不可變。如果我們想要修改這個變數:
1 |
scala> columbus = 1500 <console>:8: error: reassignment to val columbus = 1500 ^ |
請注意錯誤訊息精確地指出了錯誤位於行的哪個位置。再嘗試宣告這個變數,但這一次用“var”,讓其可變更。這樣編譯器擁有足夠的智慧來推斷出1492是一個整數,你也就不需要指定型別了:
1 2 3 |
scala> var columbus = 1492 columbus: Int = 1492 scala> columbus = 1500 columbus: Int = 1500 |
接下來,我們來定義一個類:
1 |
scala> case class Employee( name:String="guest", age:Int=30, company:String = "DevCode" ) defined class Employee |
我們定義了一個類,名為Employee,有三個不可變更的欄位:name、age和company,各自有自己的預設值。關鍵字“case”相當於Java裡的switch語句,只不過要更為靈活。它說明該類具有模式匹配的額外機制,以及其他一些特性,包括用來建立例項的工廠方法(不需要使用“new”關鍵字來構造),同樣的也不需要建立預設的getter方法。與Java中不同的是,變數預設下的訪問控制是public(而不是protected),而Scala為公開變數建立一個getter方法,並命名為變數名。如果你願意,你也可以把欄位定義成可變且/或私有(private)的,只需要在引數之前使用“var”(例如:case class Person(private var name:String))。
我們再來用不同方式建立一些例項,看看其他的特性,像是命名引數和預設引數(從Scala2.8開始引入):
1 2 3 |
scala> val guest = Employee() guest: Employee = Employee(guest,30,DevCode) scala> val guestAge = guest.age // (age變數的預設getter方法) guestAge: Int = 300scala> val anna = Employee("Anna") anna: Employee = Employee(Anna,30,DevCode)scala> val thomas = Employee("Thomas",41) thomas: Employee = Employee(Thomas,41,DevCode)scala> val luke = Employee("Luke", company="LucasArt") luke: Employee = Employee(Luke,30,LucasArt)scala> val yoda = luke.copy("Yoda", age=800) yoda: Employee = Employee(Yoda,800,LucasArt) |
不過,下面的寫法
1 |
scala> val darth = Employee("Darth", "DevCode") <console>:9: error: type mismatch; found : java.lang.String("DevCode") required: Int Error occurred in an application involving default arguments. val darth = Employee("Darth", "DevCode") |
是行不通的(可不是因為Darth不是DevCode的僱員!),這是由於建構函式在這個位置需要age作為引數,因為函式引數沒有顯性地進行命名。
現在我們再來看集合,這才是真正讓人興奮的地方。
有了泛型(Java5以上),Java可以遍歷一個——比方說整數型列表,用下面這樣的程式碼:
1 |
List<integer> numbers = new arrayList<integer>(); numbers.add(1); numbers.add(2); numbers.add(3); for(Integer n:numbers) { System.out.println("Number "+n);} |
執行的結果是
1 |
<console><console><integer><integer></integer></integer></console></console>Number 1Number 2Number 3 |
Scala對於可變集合和不可變集合進行了系統性地區別處理,不過鼓勵使用不可變集合,也因此在預設情況下建立不可變集合。這些集合是通過模擬的方式實現新增、更新和刪除操作,在這些操作中,不是修改集合,而是返回新的集合。
與前面的Java程式碼等價的Scala程式碼可能像下面這樣:
1 2 3 |
scala> val numbers = List(1,2,3) numbers: List[Int] = List(1, 2, 3) scala> for (n <- numbers) println("Number "+n) Number 1 Number 2 Number 3 |
這裡的“for”迴圈語法結構非常接近於Java的指令式程式設計風格。在Scala(以及Java虛擬機器上其他很多語言如:Groovy、JRuby或JPython)裡還有另外一種方式來實現上面的邏輯。這種方式使用一種更加偏向函式程式設計的風格,引入了Lambda表示式(有時也稱為閉包——closure)。簡單地說,Lambda表示式就是你可以拿來當作引數傳遞的函式。這些函式使用引數作為輸入(在我們的例子中就是“n”整型變數),返回語句作為函式體的最終語句。他們的形式如下
1 2 3 |
functionName { input => body} scala> numbers.foreach { n:Int => // 按Enter鍵繼續下一行 | println("Number "+n) | } Number 1 Number 2 Number 3 |
上面的例子中,函式體只有一條語句(println……),返回的是單位(Unit,也就是“空結果”),也就是大致相當於Java中的void,不過有一點不同的是——void是不返回任何結果的。
除了只列印出我們的數值列表以外,應該說我們更想做的是處理和變換這些元素,這時我們需要呼叫方法來生成結果列表,以便後面接著使用。讓我們嘗試一些例子:
1 2 3 |
scala> val reversedList = numbers.reverse reversedList: List[Int] = List(3, 2, 1) scala> val numbersLessThan3 = numbers.filter { n => n < 3 } numbersLessThan3: List[Int] = List(1, 2)scala> val oddNumbers = numbers.filterNot { n => n % 2 == 0 } oddNumbers: List[Int] = List(1, 3)scala> val higherNumbers = numbers.map { n => n + 10 } higherNumbers: List[Int] = List(11, 12, 13) |
最後的這一個變換“map”非常有用,它對列表的每一個元素應用閉包,結果是一個同樣大小的、包含了每個變換後元素的列表。
我們在這裡還想介紹最後的一個方法,就是“foldLeft”方法,它把狀態從一個元素傳播到另一個元素。比如說,要算出一個列表裡所有元素的和,你需要累加它們,並在切換元素的時候儲存中間的計數:
1 |
scala> val sumOfNumbers = numbers.foldLeft(0) { (total,element) => | total + element | } sumOfNumbers: Int = 6 |
作為第一個變數傳遞給foldLeft的值0是初始值(也就是說在把函式用到第一個列表元素的時候total=0)。(total,element)代表了一個Tuple2,在Scala裡這是一個二元組(就像要表示三維空間座標,經常要用到Tuple3(x,y,z)等等)。注意在合計時,Scala的程式設計介面實際上提供了一個“sum”方法,這樣上一條語句就可以寫成:
1 |
scala> val sumOfNumbers = numbers.sum sumOfNumbers: Int = 6 |
還有許多其他的類似的集合變換方法,你可以參照scaladoc API。你也可以把這些方法組合起來(例如:numbers.reverse.filter……),讓程式碼更加簡潔,不過這樣會影響可讀性。
最後,{ n => n + 10 }還可以簡單地寫成(_ + 10),也就是說如果輸入引數只是用於你呼叫的方法,則不需要宣告它;在我們的例子裡,“n”被稱為匿名變數,因為你可以把它用任何形式來代替,比如說“x”或者“number”,而下劃線則表示一處需要用你的列表的每個元素來填補的空白。(與“_”的功能類似,Groovy保留了關鍵字“it”,而Python則使用的是“self”)。
1 |
scala> val higherNumbers = numbers.map(_+10) higherNumbers: List[Int] = List(11, 12, 13 |
在介紹了對整數的基本處理後,我們可以邁入下一個階段,看看複雜物件集合的變換,例如使用我們上面所定義的Employee類:
1 |
scala> val allEmployees = List(luke,anna,guest,yoda,thomas) |
從這個五個元素的列表裡,我們可以應用一個條件來過濾出應用匿名方法後返回值為True的僱員,這樣就得到了——比方說屬於DevCode的僱員:
1 2 3 |
scala> val devcodeEmployees = allEmployees.filter { _.company == "DevCode" } devcodeEmployees: List[Employee] = List(Employee(Anna,30,DevCode), Employee(guest,30,DevCode), Employee(Thomas,41,DevCode)) scala> val oldEmployees = allEmployees.filter(_.age > 100).map(_.name) oldEmployees: List[String] = List(Yoda)) |
假設我們手頭的allEmployees集合是我們使用SQL查詢獲得的結果集,查詢語句可能類似於“SELECT * FROM employees WHERE company = ‘DevCode’ ”。現在我們可以把List[Employee]變換到以company名稱作為鍵、屬於該公司的所有員工的列表作為值的Map型別,這樣就可以把僱員按company來排序:
1 |
scala> val sortedEmployees = allEmployees.groupBy(_.company) sortedEmployees: scala.collection.immutable.Map[String,List[Employee]] = Map(DevCode - > List(Employee(Anna,30,DevCode), Employee(guest,30,DevCode), Employee(Thomas,41,DevCode)), LucasArt -> List(Employee(Luke,30,LucasArt), Employee(Yoda,800,LucasArt))) |
每一個列表已經作為一個值存入了(鍵——值)雜湊表,為了示範如何進一步處理這些列表,可以設想我們需要計算每個公司的僱員平均年齡。
這具體意味著我們必須要計算每個列表的每個僱員的的“age”欄位的和,然後除以該列表中僱員的數量。讓我們先計算一下DevCode:
1 2 3 |
scala> devcodeEmployees res4: List[Employee] = List(Employee(Anna,30,DevCode), Employee(guest,30,DevCode), Employee(Thomas,41,DevCode)) scala> val devcodeAges = devcodeEmployees.map(_.age) devcodeAges: List[Int] = List(30, 30, 41)scala> val devcodeAverageAge = devcodeAges.sum / devcodeAges.size devcodeAverageAge: Int = 33 |
回到我們的Map (key:String ->value:List[Employee]),下面是個更加一般性的例子。我們現在可以歸併並計算每個公司的平均年齡,要做的只是寫幾行程式碼:
1 |
scala> val averageAgeByCompany = sortedEmployees.map{ case(key,value)=> | value(0).copy(name="average",age=(value.map(_.age).sum)/value.size)} averageAgeByCompany: scala.collection.immutable.Iterable[Employee] = List(Employee(average,33,DevCode), Employee(average,415,LucasArt)) |
這裡的“case(key,value)”說明了Scala提供的模式匹配機制是多麼強大。請參考Scala的文件來獲取更多的資訊。
到這裡我們的任務就完成了。我們實現的是一個簡單的Map-Reduce演算法。由於每個公司僱員的歸併是完全獨立於其他公司,這個演算法非常直觀地實現了平行計算。