函數(shù)式風(fēng)格的程序傾向于需要更少的傳統(tǒng)的控制結(jié)構(gòu),并且使用聲明式風(fēng)格寫(xiě)的程序讀起來(lái)更好。這通常意味著打破你的邏輯,拆分到若干個(gè)小的方法或函數(shù),用匹配表達(dá)式(match expression)把他們粘在一起。函數(shù)式程序也傾向于更多面向表達(dá)式(expression-oriented):條件分支是同一類型的值計(jì)算,for(..) yield 表達(dá)式,以及遞歸都是司空見(jiàn)慣的。
用遞歸術(shù)語(yǔ)來(lái)表達(dá)你的問(wèn)題常常會(huì)使問(wèn)題簡(jiǎn)化,如果應(yīng)用了尾遞歸優(yōu)化(可以通過(guò)@tailrec注釋檢測(cè)),編譯器甚至?xí)⒛愕拇a轉(zhuǎn)換為正常的循環(huán)。對(duì)比一個(gè)標(biāo)準(zhǔn)的命令式版本的堆排序(fix-down):
def fixDown(heap: Array[T], m: Int, n: Int): Unit = {
var k: Int = m
while (n >= 2*k) {
var j = 2*k
if (j < n && heap(j) < heap(j + 1))
j += 1
if (heap(k) >= heap(j))
return
else {
swap(heap, k, j)
k = j
}
}
}
每次進(jìn)入while循環(huán),我們工作在前一次迭代時(shí)污染過(guò)的狀態(tài)。每個(gè)變量的值是那一分支所進(jìn)入函數(shù),當(dāng)找到正確的位置時(shí)會(huì)在循環(huán)中返回。 (敏銳的讀者會(huì)在Dijkstra的“Go To聲明是有害的”一文找到相似的觀點(diǎn))
考慮尾遞歸的實(shí)現(xiàn)[2]:
@tailrec
final def fixDown(heap: Array[T], i: Int, j: Int) {
if (j < i*2) return
val m = if (j == i*2 || heap(2*i) < heap(2*i+1)) 2*i else 2*i + 1
if (heap(m) < heap(i)) {
swap(heap, i, m)
fixDown(heap, m, j)
}
}
每次迭代都是一個(gè)明確定義的歷史清白的變量,并且沒(méi)有引用單元:到處都是不變的(invariants)。更容易實(shí)現(xiàn),也容易閱讀。也沒(méi)有性能方面的懲罰:因?yàn)榉椒ㄊ俏策f歸的,編譯器會(huì)轉(zhuǎn)換為標(biāo)準(zhǔn)的命令式的循環(huán)。
并不是說(shuō)命令式結(jié)構(gòu)沒(méi)有價(jià)值。在很多例子中它們很適合于提前終止計(jì)算而非對(duì)每個(gè)可能終止的點(diǎn)存在一個(gè)條件分支:的確在上面的fixDown函數(shù),如果我們已經(jīng)在堆的結(jié)尾,一個(gè)return用于提前終止。
Returns可以用于切斷分支和建立不變量(establish invariants)。這減少了嵌套,并且容易推斷后續(xù)的代碼的正確性,從而幫助了讀者。這尤其適用于衛(wèi)語(yǔ)句(guard clauses):
def compare(a: AnyRef, b: AnyRef): Int = {
if (a eq b)
return 0
val d = System.identityHashCode(a) compare System.identityHashCode(b)
if (d != 0)
return d
// slow path..
}
使用return增加了可讀性
def suffix(i: Int) = {
if (i == 1) return "st"
else if (i == 2) return "nd"
else if (i == 3) return "rd"
else return "th"
}
上面是針對(duì)命令式語(yǔ)言的,在Scala中鼓勵(lì)省略return
def suffix(i: Int) =
if (i == 1) "st"
else if (i == 2) "nd"
else if (i == 3) "rd"
else "th"
但使用模式匹配更好:
def suffix(i: Int) = i match {
case 1 => "st"
case 2 => "nd"
case 3 => "rd"
case _ => "th"
}
注意,return會(huì)有隱性開(kāi)銷:當(dāng)在閉包內(nèi)部使用時(shí)。
seq foreach { elem =>
if (elem.isLast)
return
// process...
}
在字節(jié)碼層實(shí)現(xiàn)為一個(gè)異常的捕獲/聲明(catching/throwing)對(duì),用在頻繁的執(zhí)行的代碼中,會(huì)有性能影響。
for對(duì)循環(huán)和聚集提供了簡(jiǎn)潔和自然的表達(dá)。 它在扁平化(flattening)很多序列時(shí)特別有用。for語(yǔ)法通過(guò)分配和派發(fā)閉包隱藏了底層的機(jī)制。這會(huì)導(dǎo)致意外的開(kāi)銷和語(yǔ)義;例如:
for (item <- container) {
if (item != 2) return
}
如果容器延遲計(jì)算(delays computation)會(huì)引起運(yùn)行時(shí)錯(cuò)誤,使返回不在本地上下文 (making the return nonlocal)
因?yàn)檫@些原因,常常更可取的是直接調(diào)用foreach, flatMap, map和filter —— 但在其意義清楚的時(shí)候使用for。
要求(require)和斷言(assert)都起到可執(zhí)行文檔的作用。兩者都在類型系統(tǒng)不能表達(dá)所要求的不變量(invariants)的場(chǎng)景里有用。 assert用于代碼假設(shè)的不變量(invariants) (內(nèi)部或外部的) 例如:(譯注,不變量 invariant 是指類型不可變,即不支持協(xié)變或逆變的類型變量)
val stream = getClass.getResourceAsStream("someclassdata")
assert(stream != null)
相反,require用于表達(dá)API契約:
def fib(n: Int) = {
require(n > 0)
...
}
更多建議: