到處吹噓Scala很長時間了,卻一直沒有在自己的blog上增加過任何相關內容,今天就來補一補。當然,Scala的基本特性就不多廢話了,網上已經有相當多的資料,如果懶得google,只想看完本文,那么你只需要知道它是一門以JVM為目標運行環境的靜態編程語言(當然,Scala官方也有.NET版,但已不是其重點),同時具備面向對象和函數式編程語言的特點。本文將通過一個簡單的示例,展示Scala的FP能力,其中十分heavy的用到了implicit隱式轉換和模式匹配。
代碼是從guithub的gist上抄的,簡單改了改,原始代碼見: http://gist.github.com/66925
import scala.reflect.Manifest
def simpleName(m:Manifest[_]):String = {
val name = m.toString
name.substring(name.lastIndexOf('$')+1)
}
trait Num
final class Succ[Pre<:Num] extends Num
final class _1 extends Num
type _2 = Succ[_1]
type _3 = Succ[_2]
type _4 = Succ[_3]
case class Move[N<:Num,A,B,C]()
implicit def move1[A,B,C](implicit a:Manifest[A], b:Manifest[B]) : Move[_1,A,B,C] = {
System.out.println("Move from " + simpleName(a) + " to " + simpleName(b))
null
}
implicit def moveN[P<:Num,A,B,C](implicit m1:Move[P,A,C,B], m2:Move[_1,A,B,C], m3:Move[P,C,B,A]) : Move[Succ[P],A,B,C] = null
def run[N<:Num,A,B,C](implicit m:Move[N,A,B,C]) = null
case class Left()
case class Center()
case class Right()
run[_3,Left,Right,Center]
這段代碼解決的是經典的漢諾塔問題,通過一根中柱,將左柱上64個大小依次疊加的圓盤全部移動到右柱,要求整個過程中每次只能移動一個圓盤,且每個圓盤只能獨立占據一根柱子或是疊加在比自身更大的圓盤上。
簡單分析一下就知道,這是一個遞歸問題(FP的英雄特長):
- 當只有1個圓盤時,不用通過中柱,直接可以從左柱移動到右柱;
- 當有2個圓盤時,將小盤移動到中柱,剩下的大盤移動到右柱,再從中柱把小盤移動到右柱;
- 當有3個圓盤時,先移動2個圓盤到中柱,再移動大盤到右柱,再移動2個圓盤到右柱;
- ...
很容易發現一個pattern,那就是移動N(N>1)個圓盤,可以通過以下三個步驟:
- 移動N-1個圓盤,從左柱到中柱;
- 移動剩下的1個圓盤,從左柱到右柱;
- 移動N-1個圓盤,從中柱到右柱。
在解釋代碼之前,先說說Scala的implicit隱式轉換,這是一個非常powerful的idea,當Scala編譯器發現類型不匹配,它不會直接fail,而是嘗試從代碼中指定的,在scope內的implicit轉換定義,來替換問題對象或表達式以滿足類型要求,當然,為了避免歧義,同一時刻Scala需要找到唯一的一個滿足條件的implicit定義。
我們的代碼首先定義了一個取得友好類名的方法,不去深究它,然后定義了一個正整數的序列,也不去深究它了,你只需要當作他們是正整數就好,接觸過FP的同學應該對此類定義不陌生,接下來定義了如下3個支持implicit傳入參數的方法:
- implicit def move1[A,B,C](implicit a:Manifest[A], b:Manifest[B]) : Move[_1,A,B,C]
- implicit def moveN[P<:Num,A,B,C]( implicit
m1:Move[P,A,C,B],
m2:Move[_1,A,B,C],
m3:Move[P,C,B,A]
) : Move[Succ[P],A,B,C]
- def run[N<:Num,A,B,C](implicit m:Move[N,A,B,C])
含義分別是:
- 當需要Move[_1,A,B,C]值時,可以找我幫忙(我有個side-effect,那就是輸出移動指令);
- 當需要Move[Succ[P],A,B,C]時,可以找我幫忙;
- 運行我,我接受Move[N,A,B,C]類型的參數。
簡單說明:Scala用[]表示類型參數,區別于Java的<>,另外,Scala的類型聲明在變量/函數定義之后。Move[_,A,B,C]的含義是通過C,從A移動圓盤到B。
我們來模擬運行一下,為了演示效果,用一個中等復雜的數目,3個圓盤,從Left移動到Right。
run[_3,Left,Right,Center],對應Move[Succ[_2],Left,Right,Center],于是展開成三個Move:
Move[_2,Left,Center,Right]即Move[Succ[_1],Left,Center,Right]
Move[_1,Left,Right,Center]
Move[_2,Center,Right,Left]即Move[Succ[_1],Center,Right,Left]
然后繼續展開Move[_2,Left,Center,Right]和Move[_2,Center,Right,Left],得到:
Move[_1,Left,Right,Center]
Move[_1,Left,Center,Right]
Move[_1,Right,Center,Left]
--------------------------
Move[_1,Left,Right,Center]
--------------------------
Move[_1,Center,Left,Right]
Move[_1,Center,Right,Left]
Move[_1,Left,Right,Center]
這個時候已經全部都匹配Move[_1,A,B,C],于是我們很容易得到以下輸出:
Move from Left to Right
Move from Left to Center
Move from Right to Center
Move from Left to Right
Move from Center to Left
Move from Center to Right
Move from Left to Right
希望本文能帶給Scala初學者一些感性認識和啟發。