函數式風格的程序傾向于需要更少的傳統的控制結構,并且使用聲明式風格寫的程序讀起來更好。這通常意味著打破你的邏輯,拆分到若干個小的方法或函數,用匹配表達式(match expression)把他們粘在一起。函數式程序也傾向于更多面向表達式(expression-oriented):條件分支是同一類型的值計算,for(..) yield 表達式,以及遞歸都是司空見慣的。
### 遞歸
*用遞歸術語來表達你的問題常常會使問題簡化*,如果應用了尾遞歸優化(可以通過@tailrec注釋檢測),編譯器甚至會將你的代碼轉換為正常的循環。對比一個標準的命令式版本的堆排序(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
}
}
}
~~~
每次進入while循環,我們工作在前一次迭代時污染過的狀態。每個變量的值是那一分支所進入函數,當找到正確的位置時會在循環中返回。 (敏銳的讀者會在Dijkstra的[“Go To聲明是有害的”](http://www.u.arizona.edu/~rubinson/copyright_violations/Go_To_Considered_Harmful.html)一文找到相似的觀點)
考慮尾遞歸的實現[[2]](http://twitter.github.io/effectivescala/index-cn.html#fn2 "Jump to note 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)
}
}
~~~
每次迭代都是一個明確定義的歷史清白的變量,并且沒有引用單元:到處都是不變的(invariants)。更容易實現,也容易閱讀。也沒有性能方面的懲罰:因為方法是尾遞歸的,編譯器會轉換為標準的命令式的循環。
### 返回(Return)
并不是說命令式結構沒有價值。在很多例子中它們很適合于提前終止計算而非對每個可能終止的點存在一個條件分支:的確在上面的fixDown函數,如果我們已經在堆的結尾,一個return用于提前終止。
Returns可以用于切斷分支和建立不變量(establish invariants)。這減少了嵌套,并且容易推斷后續的代碼的正確性,從而幫助了讀者。這尤其適用于衛語句(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"
}
~~~
上面是針對命令式語言的,在Scala中鼓勵省略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會有隱性開銷:當在閉包內部使用時。
~~~
seq foreach { elem =>
if (elem.isLast)
return
// process...
}
~~~
在字節碼層實現為一個異常的捕獲/聲明(catching/throwing)對,用在頻繁的執行的代碼中,會有性能影響。
### for循環和for推導
for對循環和聚集提供了簡潔和自然的表達。 它在扁平化(flattening)很多序列時特別有用。for語法通過分配和派發閉包隱藏了底層的機制。這會導致意外的開銷和語義;例如:
~~~
for (item <- container) {
if (item != 2) return
}
~~~
如果容器延遲計算(delays computation)會引起運行時錯誤,使返回不在本地上下文 (making the return nonlocal)
因為這些原因,常常更可取的是直接調用foreach, flatMap, map和filter —— 但在其意義清楚的時候使用for。
### 要求require和斷言(assert)
要求(require)和斷言(assert)都起到可執行文檔的作用。兩者都在類型系統不能表達所要求的不變量(invariants)的場景里有用。 assert用于代碼假設的不變量(invariants) (內部或外部的) 例如:(譯注,不變量 invariant 是指類型不可變,即不支持協變或逆變的類型變量)
~~~
val stream = getClass.getResourceAsStream("someclassdata")
assert(stream != null)
~~~
相反,require用于表達API契約:
~~~
def fib(n: Int) = {
require(n > 0)
...
}
~~~