Effective Scala 中文版
Effective Scala
Marius Eriksen, 推ter Inc.marius@推ter.com (@marius)
[translated by hongjiang(@hongjiang)]
Table of Contents
- 序言
- 格式化: 空格, 命名, Imports, 花括號, 模式匹配, 注釋
- 類型和泛型: 返回類型注解(annotation), 變型, 類型別名, 隱式轉換
- 集合: 層級, 集合的使用, 風格, 性能, Java集合
- 并發: Future, 集合
- 控制結構: 遞歸, 返回(Return), for循環和for推導, require和斷言(assert)
- 函數式編程: Case類模擬代數數據類型, Options, 模式匹配, 偏函數, 解構綁定, 惰性賦值, 傳名調用, flatMap
- 面向對象的編程: 依賴注入, Trait, 可見性, 結構類型
- 垃圾回收
- Java 兼容性
- 推ter標準庫: Future, Offer/Broker
- 致謝 </ul>
- Scala School
- Learning Scala
- Learning Scala in Small Bites </ul>
其他語言
序言
Scala是推ter使用的主要應用編程語言之一。很多我們的基礎架構都是用Scala寫的,我們也有一些大的庫支持我們使用。Scala是一門高效并且龐大(large)的語言,經驗教會我們在實踐中要非常小心。 它有什么陷阱?哪些特性我們應該擁抱,哪些應該避開?我們什么時候采用“純函數式風格”,什么時候應該避免?換句話說:我們發現哪些可以高效的使用這門語言的地方?本指南試圖把我們的經驗提煉成短文,提供一系列最佳實踐。我們使用Scala主要創建一些大容量分布式系統服務——我們的建議也偏向于此——但這里的大多建議也應該自然的適用其他系統。這不是法則,但有違于它的做法須有足夠的理由。
Scala提供很多工具使表達式可以很簡潔。敲的少讀的就少,讀的少就能讀的快,簡潔使代碼更清晰。然而簡潔也是一把鈍器(blunt tool)也可能起到相反的效果:在考慮正確性之后,也要為讀者著想。
首先,用Scala編程,你不是在寫Java,Haskell或Python;Scala程序不像這其中的任何一種。為了高效的使用語言,你必須用其術語表達你的問題。 強制把Java程序轉成Scala程序是無用的,因為大多數情況下它會不如原來的。
這不是對Scala的一篇介紹,我們假定讀者熟悉這門語言。這兒有些學習Scala的資源:
這是一篇“活的”文檔,我們會更新它,以反映我們當前的最佳實踐,但核心的思想不太可能會變: 永遠重視可讀性;寫泛化的代碼但不要在犧牲清晰度;利用簡單的語言特性的威力,但避免晦澀難懂(尤其是類型系統)。最重要的,總要意識到你所做的取舍。一門成熟的(sophisticated)語言需要復雜的實現,復雜性又產生了復雜性:推理,語義,特性之間的交互,以及與你合作者之間的理解。因此復雜性是為成熟所交的稅——你必須確保效用超過它的成本。
玩的愉快。
格式化
代碼格式化的規范 - 只要它們實用,并不重要。它的定義形式沒有先天的好與壞,幾乎每個人都有自己的偏好。然而,對于一致的應用采用同一格式化規則的總會增加可讀性。已經熟悉某種特定風格的讀者不必非要去掌握另一套當地習慣,或譯解另一個角落里的語言語法。
這對Scala來說也特別重要,因為它的語法高度的重疊。一個例子是方法調用:方法調用可以用“.”后邊跟圓括號,或不使用“.”后邊用空格加不帶圓括號(針對空元或一元方法)方式調用。此外,不同風格的方法調用揭露了它們在語法上不同的分歧(ambiguities)。當然一致的應用慎重的選擇一組格式化規則,對人和機器來說都會消除大量的歧義。
</a>我們依著Scala style guide 增加了以下規則:
空格
用兩個空格縮進。避免每行長度超過100列。在兩個方法、類、對象定義之間使用一個空白行。
</a>命名
對作用域較短的變量使用短名字:is,js 和ks等可出現在循環中。對作用域較長的變量使用長名字:外部APIs應該用長的,不需加以說明便可理解的名字。例如:Future.collect而非Future.all使用通用的縮寫,避開隱秘難懂的縮寫:例如每個人都知道ok,err,defn等縮寫的意思,而sfri是不常用的。不要在不同用途時重用同樣的名字:使用val(注:Scala中的不可變類型)避免用`聲明保留字變量:用typ替代`type`用主動語態(active)來命名有副作用的操作:user.activate()而非user.setActive()對有返回值的方法用可描述的名字:src.idDefined而非src.definedgetters不采用前綴get:用get是多余的:site.count而非site.getCount不必重復已經被package或object封裝過的名字:使用:object User {
def get(id: Int): Option[User]
}而非: object User {
def getUser(id: Int): Option[User]
}相比get方法getUser方法中的User是多余的,并不能提供額外的信息。 Imports
對import行按字母順序排序:這對視覺上的檢查很方便,對自動操作也很簡單。當從一個包中引入多個時,用花括號:import com.推ter.concurrent.{Broker, Offer}當引入超過6個時使用通配符:e.g.:import com.推ter.concurrent._不要輕率的使用: 一些包導入了太多的名字當引入集合的時候,用import scala.collections.immutable(不可變集合)或scala.collections.mutable(可變集合)修飾集合類名可變集合和不可變集合有同樣的類名.修飾名讓讀者能明確知道使用的是哪個變體(variant)(e.g. "immutable.Map")不要使用來自其它包的相對引用:避免
import com.推ter import concurrent而應該用清晰的:
import com.推ter.concurrent(譯注,實際上面的import不能編譯通過,第二個import應該為:import 推ter.concurrent 即import一個包實際是定義了這個包的別名。)將import放在文件的頭部:讀者可以在一個地方參考所有的引用。
花括號
花括號用于創建復合表達式,復合表達式的返回值是最后一個表達式。避免對簡單的表達式采用花括號;寫成:
def square(x: Int) = x*x
而不是:
def square(x: Int) = {
x * x
} 盡管它用在區分方法體的語句構成很誘人。第一種選擇更少凌亂,更容易讀。避免語句上的繁文縟節,除非需要闡明。
</a>模式匹配
盡可能直接在函數定義的地方使用模式匹配。例如,下面的寫法 match應該被折疊起來(collapse)
list map { item =>
item match {
case Some(x) => x
case None => default
}
} 用下面的寫法替代:
list map {
case Some(x) => x
case None => default
} 它很清晰的表達了 list中的元素都被映射,間接的方式讓人不容易明白。
</a>注釋
/**
- ServiceBuilder builds services
- ...
*/</pre>
而非標準的ScalaDoc風格:
/** ServiceBuilder builds services
... */</pre>
不要訴諸于ASCII碼藝術或其他可視化修飾。用文檔記錄APIs但不要添加不必要的注釋。如果你發現你自己添加注釋解釋你的代碼行為,先問問自己是否可以調整結構以讓它明顯的可以看出做了什么。相對于“it works, obviously” 更偏向于“obviously it works”
類型和泛型
類型系統的首要目的是檢測程序錯誤,類型系統有效的提供了一個靜態檢測的有限形式,允許我們代碼中明確某種類型的變量并且編譯器可以驗證。類型系統當然也提供了其他好處,但錯誤檢測是他存在的理由(Raison d’être)
我們對類型系統的使用反映了這一目標,但需要留心讀者:明智的使用類型可以增加清晰度,而過于聰明只會使人迷惑(譯注:應該是指過分使用Scala的類型推斷而省略了太多類型聲明)。
Scala的強大類型系統是學術探索和實踐共同來源(eg.Type level programming in Scala) 。但這是一個迷人的學術話題,這些技術很少在應用和正式產品代碼中使用。它們應該避免。
返回類型注解(annotation)
Scala允許省略返回類型,而注解提供了很好的文檔:這對public方法特別重要。而一個方法不需要對外暴露并且它的返回值類型是顯而易見的,則可以直接省略。
在使用混入(mixin)實例化對象時這一點尤其重要,Scala編譯器為這些創造了單類。例如:
trait Service def make() = new Service { def getId = 123 }上面的make不需要定義返回類型為Service;編譯器會創建一個加工過的類型: Object with Service{def getId:Int}. (譯注:with是Scala里的mixin的語法)而不必用一個顯式的注解:
def make(): Service = new Service{}現在作者不必改變make方法的公開類型而隨意的混入(mix in) 更多的特質(traits),使向后兼容很容易實現。
變型
變型(Variance)發生在泛型與子類型化(subtyping)結合的時候。與容器類型的子類型化有關,它們定義了對所包含的類型如何子類型化。因為Scala有聲明點變型(declaration site variance)注釋,公共庫的作者——特別是集合——必須有豐富的注釋器。這些注釋對共享代碼的可用性很重要,但濫用也會很危險。
不可變(invariants)是Scala類型系統中高級部分,但也是必須的一面,應該使用廣泛的(并且正確的),它有助于子類型化的應用。
不可變(Immutable)集合應該是協變的(covariant)。方法接受的類型應該適當的降級(downgrade):
trait Collection[+T] { def add[U >: T](other: U): Collection[U] }可變(mutable)集合應該是不可變的(invariant). 協變對于可變集合是典型無效的。考慮:
trait HashSet[+T] { def add[U >: T](item: U) }下面的類型層級:
trait Mammal trait Dog extends Mammal trait Cat extends Mammal
如果我現在有一個狗(dog)的 HashSet:
val dogs: HashSet[Dog]
當它為一個哺乳動物的Set,增加一只貓(cat)
val mammals: HashSet[Mammal] = dogs mammals.add(new Cat{})這將不再是一個只存儲狗(dog)的HashSet!
類型別名
使用類型別名當它們提供了便捷的命名或闡明意圖時,但對于自解釋類型不要使用類型別名。比如
() => Int
比下面定義的別名IntMarker更清晰
type IntMaker = () => Int IntMaker
但,下面的別名:
class ConcurrentPool[K, V] { type Queue = ConcurrentLinkedQueue[V] type Map = ConcurrentHashMap[K, Queue] ... }有助于交流的目的并使得更加簡短。
當使用類型別名的時候不要使用子類型化(subtyping)
trait SocketFactory extends (SocketAddress => Socket)
SocketFactory 是一個生產Socket的方法。使用一個類型別名更好:
type SocketFactory = SocketAddress => Socket
我們現在可以對 SocketFactory類型的值 提供函數字面量(function literals) ,也可以使用函數組合:
val addrToInet: SocketAddress => Long val inetToSocket: Long => Socket
val factory: SocketFactory = addrToInet andThen inetToSocket</pre>
類型別名通過用 package object 將名字綁定在頂層:
package com.推ter package object net { type SocketFactory = (SocketAddress) => Socket }注意類型別名不是新類型——他們等價于在語法上的用別名代替了原類型。
</a>隱式轉換
隱式轉換是類型系統里一個強大的功能,但應當謹慎的使用。它們有復雜的解決規則為難你——通過簡單的詞法檢查——領會實際發生了什么。在下面的場景使用隱式轉換是OK的:
- 擴展或增加一個Scala風格的集合
- 適配或擴展一個對象(pimp my library模式)(譯注參見:http://www.artima.com/weblogs/viewpost.jsp?thread=179766)
- 通過提供約束證據來加強類型安全。(Use to enhance type safety by providing constraint evidence)
- To provide type evidence (typeclassing,不知怎么翻譯,haskell中的概念,主要通過隱式轉換來實現)
- For Manifests (注:Manifest[T]包含類型T的運行時信息)
如果你發現自己在用隱式轉換,總要問問自己是否不使用這種方式也可以達到目的。
不要使用隱式轉換對兩個相似的數據類型做自動轉換(例如,把list轉換為stream);顯示的做更好,因為類型有不同的語意,讀者應該意識到這種含義。譯注: 1)一些單詞的意義不同,但翻譯為中文時可能用的相似的詞語,比如mutable, Immutable 這兩個翻譯為可變和不可變,它們是指數據的可變與不可變。 variance, invariant 也翻譯為 可變和不可變,(variance也翻譯為“變型”),它們是指類型的可變與不可變。variance指支持協變或逆變的類型,invariant則相反。
集合
Scala有一個非常通用,豐富,強大,可組合的集合庫;集合是高階的(high level)并暴露了一大套操作方法。很多集合的處理和轉換可以被表達的簡潔又可讀,但粗心的用它的功能也導致相反的結果。每個Scala程序員應該閱讀 集合設計文檔;通過它可以很好的洞察集合庫,并了解設計動機。
總使用最簡單的集合來滿足你的需求
層級
集合庫很大:除了精心設計的層級(Hierarchy)——根是 Traversable[T] —— 大多數集合都有不可變(immutable)和可變(mutable)兩種變體。無論其復雜性,下面的圖表包含了可變和不可變集合層級的重要差異。
Iterable[T] 是所有可遍歷的集合,它提供了迭代的方法(foreach)。Seq[T] 是有序集合,Set[T]是數學上的集合(無序且不重復),Map[T]是關聯數組,也是無序的。
集合的使用
優先使用不可變集合.不可變集合適用于大多數情況,讓程序易于理解和推斷,因為它們是引用透明的( referentially transparent )因此缺省也是線程安全的。
使用可變集合時,明確的引用可變集合的命名空間。不要用使用import scala.collection.mutable._ 然后引用 Set ,應該用下面的方式替代:
import scala.collections.mutable val set = mutable.Set()
這樣更明確在使用一個可變集合。
使用集合類型缺省的構造函數。每當你需要一個有序的序列(不需要鏈表語義),用 Seq() 等諸如此類的方法構造:
val seq = Seq(1, 2, 3) val set = Set(1, 2, 3) val map = Map(1 -> "one", 2 -> "two", 3 -> "three")
這種風格從語意上分離了集合與它的實現,讓集合庫使用更適當的類型:你需要Map,而不是必須一個紅黑樹(Red-Black Tree,注:紅黑樹TreeMap是Map的實現者)
此外,默認的構造函數通常使用專有的表達式,例如:Map() 將使用有3個成員的對象(專用的Map3類)來映射3個keys。
上面的推論是:在你自己的方法和構造函數里,適當的接受最寬泛的集合類型。通常可以歸結為一個: Iterable, Seq, Set, 或 Map.如果你的方法需要一個 sequence,使用 Seq[T],而不是List[T]
風格
函數式編程鼓勵使用流水線轉換將一個不可變的集合塑造為想要的結果。這常常會有非常簡明的方案,但也容易迷惑讀者——很難領悟作者的意圖,或跟蹤所有隱含的中間結果。例如,我們想要從一組投票結果(語言,票數)中統計不同程序語言的票數并按照得票的順序顯示:
val votes = Seq(("scala", 1), ("java", 4), ("scala", 10), ("scala", 1), ("python", 10)) val orderedVotes = votes .groupBy(_._1) .map { case (which, counts) => (which, counts.foldLeft(0)(_ + _._2)) }.toSeq .sortBy(_._2) .reverse上面的代碼簡潔并且正確,但幾乎每個讀者都不好理解作者的原本意圖。一個策略是聲明中間結果和參數:
val votesByLang = votes groupBy { case (lang, _) => lang } val sumByLang = votesByLang map { case (lang, counts) => val countsOnly = counts map { case (_, count) => count } (lang, countsOnly.sum) } val orderedVotes = sumByLang.toSeq .sortBy { case (_, count) => count } .reverse代碼也同樣簡潔,但更清晰的表達了轉換的發生(通過命名中間值),和正在操作的數據的結構(通過命名參數)。如果你擔心這種風格污染了命名空間,用大括號{}來將表達式分組:
val orderedVotes = { val votesByLang = ... ... }性能
高階集合庫(通常也伴隨高階構造)使推理性能更加困難:你越偏離直接指示計算機——即命令式風格——就越難準確預測一段代碼的性能影響。然而推理正確性通常很容易;可讀性也是加強的。在java運行時使用Scala使得情況更加復雜,Scala對你隱藏了裝箱(boxing)/拆箱 (unboxing)操作,可能引發嚴重的性能或內存空間問題。
在關注于低層次的細節之前,確保你使用的集合適合你。 確保你的數據結構沒有不期望的漸進復雜度。各種Scala集合的復雜性描述在這兒。
性能優化的第一條原則是理解你的應用為什么這么慢。不要使用空數據操作。在執行前分析[1]你的應用。關注的第一點是熱循環(hot loops) 和大數據結構.過度關注優化通常是浪費精力。記住Knuth(高德納)的格言:“過早優化是萬惡之源”。
如果是需要更高性能或者空間效率的場景,通常更適合使用低級的集合。對大序列使用數組替代列表(List) (不可變Vector提供了一個指稱透明的轉換到數組的接口) ,并考慮使用buffers替代直接序列的構造來提高性能。
Java集合
使用 scala.collection.JavaConverters 與java集合交互。有一系列的隱式的用于Java與Scala的轉換。有助于讀者,使用下面的方式確保轉換是顯式的:
import scala.collection.JavaConverters._
val list: java.util.List[Int] = Seq(1,2,3,4).asJava val buffer: scala.collection.mutable.Buffer[Int] = list.asScala</pre></a>
并發
現代服務是高度并發的—— 服務器通常是在10–100秒內并列上千個同時操作——處理隱含的復雜性是創作健壯系統軟件的中心主題。
*線程提供了一種表達并發的方式:它們給你獨立的,堆共享的(heap-sharing)由操作系統調度的執行上下文。然而,在java里線程的創建是昂貴的,是一種必須托管的資源,通常借助于線程池。這對程序員創造了額外的復雜,也造成高度的耦合:很難從所使用的基礎資源中分離應用邏輯。
這種復雜度尤其明顯當創建高度分散(fan-out)的服務時: 每個輸入請求導致一大批對另一層系統的請求。在這些系統中,線程池必須被托管以便根據每一層請求的比例來平衡:管理不善的線程池會滲入到另一個里(bleeds into another)。
一個健壯系統必須考慮超時和取消,兩者都需要引入另一個“控制”線程,使問題更加復雜。注意若線程很廉價這些問題也將會被削弱:不再需要一個線程池,超時的線程將被丟棄,不再需要額外的資源管理。
因此,資源管理危害了模塊。
Future
使用Future管理并發。它們將并發操作從資源管理里解耦出來:例如,Finagle(譯注:推ter的一個框架)以有效的方式在少量線程上實現復用(multiplexes)。Scala有一個輕量級的閉包字面語法(literal syntax),所以Futures引入了微不可察的語法開銷,它們成為很多程序員的第二本能(second nature)
Futures允許程序員用一種聲明風格,可擴充的,有處理失敗原則的,來表達并發計算。這些特性使我們相信它們尤其適合在函數式變成中用,這也是鼓勵使用的風格。
*更愿意改造future為自己創建的。Future的轉換(transformations)確保失敗會傳播,可以通過信號取消,對于程序員來說不必考慮java內存模型的含義。甚至一個仔細的程序員會寫出下面的代碼,順序的發出10次RPC請求打印結果:
val p = new Promise[List[Result]] var results: List[Result] = Nil def collect() { doRpc() onSuccess { result => results = result :: results if (results.length < 10) collect() else p.setValue(results) } onFailure { t => p.setException(t) } }collect() p onSuccess { results => printf("Got results %s\n", results.mkString(", ")) }</pre>
程序員不得不確保RPC失敗是可傳播的,代碼散布在控制流程中;糟糕的是,代碼是錯誤的! 沒有聲明results是volatile,我們不能確保results每次迭代會保持前一次值。Java內存模型是一個狡猾的野獸,幸好我們可以避開這些陷阱,通過用聲明式風格(declarative style):
def collect(results: List[Result] = Nil): Future[List[Result]] = doRpc() flatMap { result => if (results.length < 9) collect(result :: results) else result :: results }collect() onSuccess { results => printf("Got results %s\n", results.mkString(", ")) }</pre>
我們用flatMap順序的操作,把我們處理中的結果預追加(prepend)到list中。這是一個通用的函數式編程的習語的Futures譯本。這是正確的,不僅需要的“咒語”(boilerplate)可以減少,易出錯的可能性也會減少,并且讀起來更好。
*Future組合子(combinators)的使用。當操作多個futures時,Future.select,Future.join,和Future.collect應該被組合編寫出通用模式。
</a>集合
并發集合的主題充滿著意見、微妙(subtleties)、教條、恐懼/不確定/懷疑(FUD)。在大多實際場景都不存在問題:總是先用最簡單,最無聊,最標準的集合解決問題。 在你知道不能使用synchronized前不要去用一個并發集合:JVM有著復雜老練的手段來制造同步欺騙(synchronization cheap),所以它的效率能讓你驚訝。
如果一個不可變(immutable)集合可行,就盡可能用不可變集合——它們是指稱透明的(referentially transparent),所以它們用在并發上下文的理由是簡單。不可變集合的改變通常用更新引用到當前值(一個var單元或一個 AtomicReference)。必須小心正確的應用:原子型的(atomics)必須重試(retried),變量(var類型的)必須聲明為 volatile以保證它們發布(published)到它們的線程。
可變的并發集合有著復雜的語義,利用java內存模型的微妙的一面,所以在你使用前確定你理解它的含義——尤其對于發布更新(新公開方法)。同步的集合同樣寫起來更好:像getOrElseUpdate操作不能夠被并發集合正確的實現,創建復合(composite)集合尤其容易出錯。
控制結構
函數式風格的程序傾向于需要更少的傳統的控制結構,并且使用聲明式風格寫的程序讀起來更好。這通常意味著打破你的邏輯,拆分到若干個小的方法或函數,用用匹配表達式(match expression)把他們粘在一起。函數式程序也傾向于更多面向表達式(expression-oriented):條件分支是同一類型的值計算,for表達式(for-comprehension),遞歸都是司空見慣的。
遞歸
*用遞歸術語來表達你的問題常常會是簡化的,如果應用了尾遞歸優化(可以通過@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循環,我們工作在前一次迭代時污染過的狀態。每個變量的值是那一分支所進入函數,當找到正確的位置時會在循環中return。 (敏銳的讀者會在Dijkstra的“Go To聲明是有害的”一文找到相似的觀點)
考慮尾遞歸的實現[2]:
@tailrec final def fixDown(heap: Array[T], i: Int, j: Int) { if (j < i*2) returnval m = if (j == i2 || heap(2i) < heap(2i+1)) 2i else 2*i + 1 if (heap(m) < heap(i)) { swap(heap, i, m) fixDown(heap, m, j) } }</pre>
每次迭代都是一個明確定義的清白歷史的變量,并且沒有引用單元:到處都是不變的(invariants)。更容易實現,也容易閱讀。也沒有性能方面的懲罰:既然方法是尾遞歸,編譯器會轉換為標準的命令式的循環。
返回(Return)
并不是說命令式結構沒有價值。在很多例子中它們很適合于提前終止計算而非對每個可能終止的點存在一個條件分支:的確在上面的fixDown函數,一個return用于提前終止如果我們已經在堆的結尾。
Returns可以用于切斷分支建立不變量(establish invariants)。這幫助了讀者減少了嵌套,并且容易實現后續的代碼的正確性。這尤其適用于衛語句(guard clauses):
def compare(a: AnyRef, b: AnyRef): Int = { if (a eq b) return 0val d = System.identityHashCode(a) compare System.identityHashCode(b) if (d != 0) return d
// slow path.. }</pre>
使用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... }</pre>
在字節碼層實現時會包含一個異常的捕獲/聲明(catching/throwing)對,用在頻繁的執行的代碼中,會有性能影響。
</a>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) ... }函數式編程
value-oriented 編程有很多優勢,特別是用在與函數式編程結構相結合。這種風格強調通過狀態變化來轉換values,生成的代碼是指稱透明的(referentially transparent),提供了更強的不變型(invariants),因此容易實現。Case類(也被翻譯為樣本類),模式匹配,解構綁定 (destructuring bindings),類型推斷,輕量級的閉包和方法創建語法都是這一行的工具。
Case類模擬代數數據類型
Case類可模擬代數數據類型(ADT)編碼:它們對大量的數據結構進行建模時有用,用強不變類型(invariants)提供了簡潔的代碼。尤其在結合模式匹配情況下。模式匹配實現了全面解析提供更強大的靜態保護。 (譯注:ADTs是Algebraic Data Type代數數據類型的縮寫,關于這個概念見我的另一篇blog)
下面是用case類模擬代數數據類型的模式
sealed trait Tree[T] case class Node[T](left: Tree[T], right: Tree[T]) extends Tree[T] case class Leaf[T](value: T) extends Tree[T]
類型 Tree[T] 有兩個構造函器:Node和Leaf。定義類型為sealed(封閉類)允許編譯器徹底的分析(這是針對模式匹配的,參考Programming in Scala)因為構造器將不能從外部源文件中添加。
與模式匹配一同,這個建模使得代碼簡潔并且顯然是正確的(obviously correct)
def findMin[T <: Ordered[T]](tree: Tree[T]) = tree match { case Node(left, right) => Seq(findMin(left), findMin(right)).min case Leaf(value) => value }一些遞歸結構,如樹的組成是典型的ADTs(代數數據類型)應用,它們的用處領域更大。 disjoint,unions特別容易的用ADTs建模;這些頻繁發生在狀態機上(state machines)。
Options
Option類型是一個容器,空(None)或滿(Some(value))二選一。它提供了使用null的另一種安全選擇,應該盡可能的替代null。它是一個集合(最多只有一個元素)并用集合操所修飾,盡量用Option。
用 var username: Option[String] = None … username = Some(“foobar”)
代替
var username: String = null ... username = "foobar"
前一種更安全:Option類型靜態的強制username必須對空(emptyness)做檢測。
對一個Option值做條件判斷應該用foreach
if (opt.isDefined) operate(opt.get)
上面的代碼應該用下面的方式替代:
opt foreach { value => operate(value)}風格看起來有些古怪,但提供了更多的安全和簡潔。如果兩種情況都有(Option的None或Some),用模式匹配
opt match { case Some(value) => operate(value) case None => defaultAction() }如果都缺少缺省值,用getOrElse方法:
operate(opt getOrElse defaultValue)
不要過度使用Option: 如果有一個明確的缺省值——一個Null對象——直接用Null而不必用Option
Option還有一個方便的構造器用于包裝空值(nullable value)
Option(getClass.getResourceAsStream("foo"))得到一個 Option[InputStream] 假定空值(None)時getResourceAsStream會返回null
模式匹配
模式匹配(x match { …) 在Scala代碼中無處不在:用于合并條件執行、解構(destructuring) 、在構造中造型。使用好模式匹配可以增加程序的明晰和安全。使用模式匹配實現類型轉換:
obj match { case str: String => ... case addr: SocketAddress => ...模式匹配聯合使用解構(destructuring)時效果最好(例如你要匹配case類);下面的寫法
animal match { case dog: Dog => "dog (%s)".format(dog.breed) case _ => animal.species }應該被替代為:
animal match { case Dog(breed) => "dog (%s)".format(breed) case other => other.species }寫自定義的抽取器(extractor)時必須有雙重構造器,否則可能是不適合的。(譯注:這句話沒理解,可能作者是想說apply方法與unapply的對稱性)
對條件執行不要用模式匹配,默認的方法更有意義。集合庫的方法通常返回Options,避免:
val x = list match { case head :: _ => head case Nil => default }因為
val x = list.headOption getOrElse default
更短并且更能表達目的
偏函數
Scala提供了定義偏函數(PartialFunction)的語法快捷:
val pf: PartialFunction[Int, String] = { case i if i%2 == 0 => "even" }它們也可能由 orElse 組成:
val tf: (Int => String) = pf orElse { case _ => "odd"}tf(1) == "odd" tf(2) == "even"</pre>
偏函數出現在很多場景,并被有效的編碼為 PartialFunction,例如 方法參數:
trait Publisher[T] { def subscribe(f: PartialFunction[T, Unit]) }val publisher: Publisher[Int] = .. publisher.subscribe { case i if isPrime(i) => println("found prime", i) case i if i%2 == 0 => count += 2 / ignore the rest / }</pre>
或在返回一個Option的情況下:
// Attempt to classify the the throwable for logging. type Classifier = Throwable => Option[java.util.logging.Level]
可以更好的用PartialFunction表達
type Classifier = PartialFunction[Throwable, java.util.Logging.Level]
因為它提供了更好的可組合性:
val classifier1: Classifier val classifier2: Classifier
val classifier = classifier1 orElse classifier2 orElse { _ => java.util.Logging.Level.FINEST }</pre></a>
解構綁定
解構綁定與模式匹配有關,它們用了相同的機制,但可應用在當匹配只有一種選項的時候 (以免你接受異常的可能). 解構綁定特別適用于元組(tuple)和樣本類(case class).
val tuple = ('a', 1) val (char, digit) = tupleval tweet = Tweet("just tweeting", Time.now) val Tweet(text, timestamp) = tweet</pre></a>
惰性賦值
當使用lazy修飾一個val成員時,其賦值情況是在需要時才賦值的(by need) 因為Scala中成員與方法是等價的(除了private[this]成員)
lazy val field = computation()
相當于下面的簡寫:
var _theField = None def field = if (_theField.isDefined) _theField.get else { _theField = Some(computation()) _theField.get }也就是說,它在需要時計算結果并會記住結果,在這種目的時使用lazy成員;但當語意上需要惰性賦值時(by semantics),要避免使用惰性賦值,這種情況下,最好顯式賦值因為它使得成本模型是明確的,并且副作用被嚴格的控制。
Lazy成員是線程安全的。
傳名調用
方法參數可以指定為傳名參數 (by-name) 意味著參數不是綁定到一個值,而是需要反復計算的。這一特性需要小心使用; 調用者按照傳值(by-value)語法期待的話會感到驚訝。這一特性的動機是構造語法自然的 DSLs——使新的控制結構看起來更像本地語言特征。
只在下面的控制結構中使用傳名調用, 調用者明顯傳遞的是一段代碼塊(block)而非一個確定的計算結果。傳名參數必須放在參數列表的最后一位。當使用傳名調用時,確保方法名稱讓調用者明顯的感知到方法參數是傳名參數。
當你想要一個值被計算多次,特別是這個計算會引起副作用時,使用顯式函數:
class SSLConnector(mkEngine: () => SSLEngine)
這樣意圖很明確,調用者不會感到驚奇。
flatMap
flatMap——結合了map 和 flatten —— 要特別小心,它有著難以琢磨的威力和強大的實用性。類似它的兄弟 map,它也是經常在非傳統的集合中使用的,例如 Future , Option.它的行為由它的簽名揭示;對于一些容器 Container[A]
flatMap[B](f: A => Container[B]): Container[B]
flatMap對集合中的每個元素調用了 函數 f 產生一個新的集合,將它們全部 flatten 后放入結果中
例如,獲取兩個字符的所有排列,相同的字符不能出現兩次
val chars = 'a' to 'z' val perms = chars flatMap { a => chars flatMap { b => if (a != b) Seq("%c%c".format(a, b)) else Seq() } }等價于下面這段更簡潔的 for-comprehension (基本就是針對上面的語法糖)
val perms = for { a <- chars b <- chars if a != b } yield "%c%c".format(a, b)flatMap在處理Options時頻繁使用—— 它將options鏈合并為一個,
val host: Option[String] = .. val port: Option[Int] = ..
val addr: Option[InetSocketAddress] = host flatMap { h => port map { p => new InetSocketAddress(h, p) } }</pre>
也可以使用更簡潔的for來實現:
val addr: Option[InetSocketAddress] = for { h <- host p <- port } yield new InetSocketAddress(h, p)</a>對flatMap在在Futures中的使用futures一節中有討論。
面向對象的編程
Scala的博大很大程度上在于它的對象系統。Scala中所有的值都是對象,就這一意義而言Scala是門純粹的語言;基本類型和組合類型沒有區別。Scala也提供了mixin的特性允許更多正交的、細粒度的構造一些在編譯時受益于靜態類型檢測的可被靈活組裝的模塊。 mixin系統的背后動機之一是消除傳統的依賴注入。這種“組件風格(component style)”編程的高潮是是the cake pattern.
依賴注入
在我們用來,發現Scala本身刪除了很多經典(構造函數)依賴注入的語法開銷,我們更愿意就這樣用: 它更清晰,依賴仍然通過類型編碼,類構造語法是如此微不足道而變得輕而易舉。有些無聊,簡單,但有效。對模塊化編程時使用依賴注入,特別是,組合優于繼承 —這使得程序更加模塊化和可測試的。當遇到需要繼承的情況,問問自己:在語言缺乏對繼承支持的情況下如何構造程序?答案可能是令人信服的。
依賴注入典型的使用到 trait
trait TweetStream { def subscribe(f: Tweet => Unit) } class HosebirdStream extends TweetStream ... class FileStream extends TweetStream ..class TweetCounter(stream: TweetStream) { stream.subscribe { tweet => count += 1 } }</pre>
這是常見的注入工廠 — 用于產生其他對象的對象。在這些例子中,更青睞用簡單的函數而非專有的工廠類型。
class FilteredTweetCounter(mkStream: Filter => TweetStream) { mkStream(PublicTweets).subscribe { tweet => publicCount += 1 } mkStream(DMs).subscribe { tweet => dmCount += 1 } }</a>Trait
依賴注入不妨礙使用公共接口,或在trait中實現公共代碼。恰恰相反—正是因為以下原因而高度鼓勵使用trait:一個具體的類可以實現多接口(traits),公共的代碼可以通過這些類復用。
保持traits簡短并且是正交的:不要把分離的功能混在一個trait里,考慮將最小的相關的意圖放在一起。例如,想象一下你要做一些IO的操作:
trait IOer { def write(bytes: Array[Byte]) def read(n: Int): Array[Byte] }分離兩個行為:
trait Reader { def read(n: Int): Array[Byte] } trait Writer { def write(bytes: Array[Byte]) }可以將它們以混入(mix)的方式實現一個IOer : new Reader with Writer
接口最小化促使更好的正交和更清晰的模塊化。
可見性
Scala有很豐富的可見性修飾。使用這些可見性修飾很重要,它們定義了哪些構成公開API。公開APIs應該限制,所以用戶不會無意中依賴實現細節并限制了作者的能力來修改它們: 它們對于好的模塊化設計是至關重要的。一般來說,擴展公開APIs比收縮公開的APIs容易的多。差勁的注釋也能危害到你代碼向后的二進制兼容性。
private[this]
一個類的成員標記為私有的,
private val x: Int = ...
它對這個類的所有實例來說都是可見的(但對其子類不可見)。大多情況,你想要的是 private[this] 。
private[this] val x: Int = ..
這個修飾限制了它只對當前特定的實例可見。Scala編譯器會把private[this]翻譯為一個字段訪問(因為訪問僅限于靜態定義的類)有時可增加性能優化。
單例類型
在Scala中創建單例類型是很常見的,例如:
def foo() = new Foo with Bar with Baz { ... }在這種情況下,可以通過聲明返回類型來限制可見性:
def foo(): Foo with Bar = new Foo with Bar with Baz { ... }foo()方法的調用者會看到以有限視圖的返回實例(Foo with Bar)
結構類型
不要在正常情況下使用結構類型。結構類型有著便利且強大的特性,但不幸的是在JVM上的實現不是很高效。然而——由于實現的怪癖——它提對執行反射(reflection)供了很好的速記。
val obj: AnyRef obj.asInstanceOf[{def close()}].close()垃圾回收
我們對生產模式的應用花了很多時間來調整垃圾回收。垃圾回收的關注點與java大致相似,盡管一些慣用的Scala代碼比起慣用的java代碼會容易產生更多(短暫的)垃圾——函數式風格的副產品。Hotspot的分代垃圾收集通常使這不成問題,因為短暫的(short-lived)垃圾在大多情形下會被有效的釋放掉。
在談GC調優話題前,先看看這個由Attila舉例說明我們在GC方面的一些經驗的介紹。
Scala固有的問題,你能夠緩解GC的方法是產生更少的垃圾;但不要在沒有數據的情況下行動。除非你做了某些明顯的惡化。使用各種java的profiling工具——我們擁有的包括heapster和gcprof。
Java 兼容性
當我們寫的Scala代碼被java調用時,我們要確保從java來用仍然習慣。這常常不需要額外的努力——class和trait明確的等價于 java的中的對應類型 —— 但有時需要提供獨立的Java Api。一種感受你的庫中的java api好的方式是用java寫單元測試(只是為了兼容性);這也確保了你的庫中的java視圖保持穩定,在這一點上不會隨著時間因Scala編譯器的波動而影響。
包含部分實現的Trait不能直接被java使用: 改為 extends 一個抽象類
// 不能直接被java使用 trait Animal { def eat(other: Animal) def eatMany(animals: Seq[Animal) = animals foreach(eat(_)) }// 改為這樣: abstract class JavaAnimal extends Animal</pre></a>
推ter標準庫
推ter最重要的標準庫是 Util 和 Finagle。Util 可以理解為Scala和Java的標準庫擴展,提供了標準庫中沒有的功能或更合適的實現。Finagle 是我們的RPC系統,核心分布式系統組件。
Future
Futures已經在并發一節中簡單討論過。有一個中心機制來協調異步處理,滲透在我們代碼庫中,包括核心的Finagle。Futures允許組合并發事件,簡化了高并發操作。有助于它們在JVM上高效的實現。
推ter的future是異步的,所以基本上任何操作(阻塞操作)可以suspend它的線程的執行;網絡IO和磁盤IO是就是例子——必須由系統處理,它為結果提供future。Finagle為網絡IO提供了這樣一種系統。
Futue清晰簡單:它們持有一個尚未完成運算結果的 promise 。它們是一個簡單的容器——一個占位符。一次計算當然可能會失敗,這種狀況必須被編碼:一個Future可以有3種狀態: pending, failed, completed。
閑話: 組合(composition)
讓我們重新審視我們所說的組合:將簡單的組件合成一個更復雜的。函數組合的一個權威的例子:給定函數 f 和 g,組合函數 (g°f)(x) = g(f(x)) ——結果先對 x使用f函數,然后在使用g函數——用Scala來寫:
val f = (i: Int) => i.toString val g = (s: String) => s+s+s val h = g compose f // : Int => String
scala> h(123) res0: java.lang.String = 123123123</pre>
復合函數h,是個新的函數,由之前定義的f和g函數合成。
</div>Future是一種集合類型——它是個包含0或1個元素的容器——你可以發現他們有標準的集合方法(eg:map, filter, foreach)。因為Future的值是延遲的,結果應用這些方法中的任何一種必然也延遲;在
val result: Future[Int] val resultStr: Future[String] = result map { i => i.toString }函數 { i => i.toString } 不會被調用,直到int值可用;轉換集合的resultStr在可用之前也一直是待定狀態。
List可以被 flatten:
val listOfList: List[List[Int]] = .. val list: List[Int] = listOfList.flatten
這對future也是有意義的:
val futureOfFuture: Future[Future[Int]] = .. val future: Future[Int] = futureOfFuture.flatten
因為future是延遲的,flatten的實現——立即返回——不得不返回一個等待外部future (Future[Future[Int]) 完成的future (Future[Future[Int]]).如果外部future失敗,內部flattened future也將失敗。
Future (類似List) 也定義了flatMap;Future[A] 定義方法flatMap的簽名
flatMap[B](f: A => Future[B]): Future[B]
如同組合 map 和 flatten,我們可以這樣實現:
def flatMap[B](f: A => Future[B]): Future[B] = { val mapped: Future[Future[B]] = this map f val flattened: Future[B] = mapped.flatten flattened }這是一種有威力的組合。使用flatMap我們可以定義一個 Future 是兩個Future序列的結果。第二個future 的計算基于第一個的結果。想象我們需要2次RPC調用來驗證一個用戶身份,我們可以用下面的方式組合操作:
def getUser(id: Int): Future[User] def authenticate(user: User): Future[Boolean]
def isIdAuthed(id: Int): Future[Boolean] = getUser(id) flatMap { user => authenticate(user) }</pre>
這種組合類型的一個額外的好處是錯誤處理是內置的:如果getUser(..)或authenticate(..)沒有額外的錯誤處理代碼 future 從 isAuthred(..)返回時將會失敗。
風格
Future回調方法(respond, onSuccess, onFailure, ensure) 返回一個新的鏈到它parent的Future,。這個Future被保證只有在它parent完成后才完成,使用模式如下:
acquireResource() onSuccess { value => computeSomething(value) } ensure { freeResource() }freeResource() 被保證只有在 computeSomething之后才執行, 允許模擬 try-finally 模式。
使用 onSuccess替代 foreach —— 它與 onFailure 方法對稱,命名的意圖更明確,并且也允許 chaining。
永遠避免直接創建Promise實例: 幾乎每一個任務都可以通過使用預定義的組合子完成。這些組合子確保錯誤和取消是可傳播的, 通常鼓勵的數據流風格的編程,不再需要同步和volatility聲明。
用尾遞歸風格編寫的代碼不再遭受堆棧空間泄漏,允許以數據流風格高效的實現循環:
case class Node(parent: Option[Node], ...) def getNode(id: Int): Future[Node] = ...
def getHierarchy(id: Int, nodes: List[Node] = Nil): Future[Node] = getNode(id) flatMap { case n@Node(Some(parent), ..) => getHierarchy(parent, n :: nodes) case n => Future.value((n :: nodes).reverse) }</pre>
Future定義很多有用的方法: 使用 Future.value() 和 Future.exception() 來創建未滿意(pre-satisfied) 的future。Future.collect(), Future.join() 和 Future.select() 提供了組合子將多個future合成一個(例如:scatter-gather操作的gather部分)。
Cancellation
Future實現了一種弱形式的取消。調用Future#cancel 不會直接終止運算,而是發送某個級別的可被任何處理查詢的觸發信號,最終滿足這個future。Cancellation信號流向相反的方向:一個由消費者設置的cancellation信號,傳播到它的消費者。生產者使用 Promise的onCancellation來監聽信號并執行相應的動作。
這意味這取消語意上依賴生產者,沒有默認的實現。取消只是一個提示。
Local
</a>Util的Local提供了一個位于特定的future派發樹(dispatch tree)的引用單元(cell)。設定一個local的值,使這個值可以用于被同一個線程的Future 延遲的任何計算。有一些類似的線程 locals,除了它們的范圍不是一個java線程,而是一個 future 線程樹。在
trait User { def name: String def incrCost(points: Int) } val user = new Local[User]...
user() = currentUser rpc() ensure { user().incrCost(10) }</pre>
在 ensure塊中的 user() 將在回調被添加的時候引用 user local的值。
就thread locals來說,Locals非常的方便,但要避免:確信通過顯式傳遞數據時問題不能被充分的解決,甚至有些繁重的。
Locals有效的被核心庫使用在非常常見的問題上——線程通過RPC跟蹤,傳播監視器,為future的回調創建stack traces——任何其他解決方法都使得用戶負擔過度。Locals在幾乎任何其他情況下都不適合。
Offer/Broker
并發系統由于需要協調訪問數據和資源而變得復雜。Actor提出一種簡化的策略:每一個actor是一個連續的進程(process),保持自己的狀態和資源,數據通過消息的方式與其它actor共享。 共享數據需要actor之間通信。
Offer/Broker 建立于這3個重要的方式上:1,通信通道(Brokers)是first class——即發送消息需要通過Brokers,而非直接到actor。2, Offer/Broker 是一種同步機制:通信會話是同步的。 這意味我們可以用 Broker做為協調機制:當進程a發送一條信息給進程b;a和b都要對系統狀態達成一致。3, 最后,通信可以選擇性的執行:一個進程可以提出幾個不同的通信,其中的一個將被獲取。
為了以一種通用的方式支持選擇性通信(以及其他組合),我們需要將通信的描述和執行解耦。這正是Offer做的——它是一個持久數據用于描述一次通信;為了執行這個通信(offer執行),我們通過它的sync()方法同步
trait Offer[T] { def sync(): Future[T] }返回 Future[T] 當通信被獲取的時候生成交換值
Broker通過offer協調值的交換——它是通信的通道:
trait Broker[T] { def send(msg: T): Offer[Unit] val recv: Offer[T] }所以,當創建2個offer
val b: Broker[Int] val sendOf = b.send(1) val recvOf = b.recv
sendOf和recvOf都同步
// In process 1: sendOf.sync()
// In process 2: recvOf.sync()</pre>
兩個offer都獲取并且值1被交換。
可選擇通信被通過Offer.choose綁定的多個offer執行。
def choose[T](ofs: Offer[T]*): Offer[T]
生成一個新的offer,當同步的獲取特定的ofs——第一個可用。當多個都立即可用時,隨機獲取一個。
Offer對象有些一次性的Offers用于與來自Broker的Offer構建。
Offer.timeout(duration): Offer[Unit]
offer在給定時間后激活。Offer.never將用于不會有效,Offer.const(value)在給定值后立即有效。通過選擇性通信來組合是非常游泳的。例如,在一個send操作中使用超時:
Offer.choose( Offer.timeout(10.seconds), broker.send("my value") ).sync()人們可能會比較 Offer/Broker 與SynchronousQueue,他們細節上不同而非重要的方面。Offer可以被組合,而queue不能。例如,考慮一組queues,描述為 Brokers:
val q0 = new Broker[Int] val q1 = new Broker[Int] val q2 = new Broker[Int]
現在讓我們為reading創建一個合并的queue
val anyq: Offer[Int] = Offer.choose(q0.recv, q1.recv, q2.recv)
anyq是一個將從第一個可用的queue中讀取的offer。注意 anyq 仍是同步的——我們仍然擁有底層隊列的語義。這類組合是不可能用queue的。
例子:一個簡單的連接池
連接池在網絡應用中很常見,并且它們的實現常常需要技巧——例如,通常需要超時機制在從池中獲取一個連接的時候,因為不同的客戶端有不同的延遲需求。池的簡單原則:維護一個連接隊列,滿足那些進入的等待者。使用傳統的同步原語,這通常需要2個隊列(queues):一個用于等待者(當沒有連接可用時),一個用于連接(當沒有等待者時)。
使用 Offer/Brokers ,可以表達的非常自然:
class Pool(conns: Seq[Conn]) { private[this] val waiters = new Broker[Conn] private[this] val returnConn = new Broker[Conn]val get: Offer[Conn] = waiters.recv def put(c: Conn) { returnConn ! c }
private[this] def loop(connq: Queue[Conn]) { Offer.choose( if (connq.isEmpty) Offer.never else { val (head, rest) = connq.dequeue waiters.send(head) { _ => loop(rest) } }, returnConn.recv { c => loop(connq enqueue c) } ).sync() }
loop(Queue.empty ++ conns) }</pre>
loop總是提供一個歸還的連接,但只有queue非空的時候才會send。 使用持久化隊列(persistent queue)更進一步簡化邏輯。與連接池的接口也是通過Offer,所以調用者如果愿意設置timeout,他們可以通過利用組合子 (combinators)來做:
val conn: Future[Option[Conn]] = Offer.choose( pool.get { conn => Some(conn) }, Offer.timeout(1.second) { _ => None } ).sync()實現timeout不需要額外的記賬(bookkeeping);這是因為Offer的語義:如果Offer.timeout被選擇,不會再有offer從池中獲得——連接池和它的調用者在各自waiter的broker上不必同時同意接受和發送。
埃拉托色尼篩子(Sieve of Eratosthenes 譯注:一種用于篩選素數的算法)
在構造并發程序作為一組順序的同步通信進程,它通常很有用——有時程序被大大的簡化了。Offer和Broker提供了一組工具來讓它簡單并一致。確實,它們的應用超越了我們可能認為是經典并發性問題——并發編程(使有Offer/Broker輔助)是一種有用的構建工具,正如子例程 (subroutines),類,和模塊都是——來自CSP的重要思想。
這里有一個埃拉托色尼篩子可以對一個整數流(stream of integers)構造為連續的應用過濾器 。首先,我們需要一個整數的源(source of integers):
def integers(from: Int): Offer[Int] = { val b = new Broker[Int] def gen(n: Int): Unit = b.send(n).sync() ensure gen(n + 1) gen(from) b.recv }integers(n) 方法簡單的提供了從n開始的所有連續的整數。然后我們需要一個過濾器:
def filter(in: Offer[Int], prime: Int): Offer[Int] = { val b = new Broker[Int] def loop() { in.sync() onSuccess { i => if (i % prime != 0) b.send(i).sync() ensure loop() else loop() } } loop()b.recv }</pre>
def sieve = { val b = new Broker[Int] def loop(of: Offer[Int]) { for (prime <- of.sync(); _ <- b.send(prime).sync()) loop(filter(of, prime)) } loop(integers(2)) b.recv }loop() 工作很簡單:從of中讀取下一個質數(prime),然后對of應用過濾器排除這個質數。loop不斷的遞歸,持續的質數被過濾,于是我們得到了篩選結果。我們現在打印前10000個質數:
val primes = sieve 0 until 10000 foreach { _ => println(primes.sync()()) }除了構造簡單,組件正交,這種做法也給你一種流式篩子(streaming sieve):你不需要先計算出你感興趣的質數集合,進一步提高了模塊化。
致謝
本課程由推ter公司Scala社區貢獻——我希望我是個忠實的記錄者。
Blake Matheny, Nick Kallen, Steve Gury, 和Raghavendra Prabhu提供了很多有用的指導和許多優秀的建議。
</a>- Yourkit 是一個很好的profiler [back]
- From Finagle’s heap balancer [back] </ol>
本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!