引入Option優(yōu)雅地保證健壯性
REA的Ken Scambler在其演講《2 Year of Real World FP at REA》中,總結(jié)了選擇函數(shù)式編程的三個(gè)原因:Modularity, Abstraction和Composability。
函數(shù)式編程強(qiáng)調(diào)純函數(shù)(Pure Function),這是模塊化的一個(gè)重要基礎(chǔ),因?yàn)閷?duì)于純函數(shù)而言,可以不用考慮調(diào)用的上下文,就可以根據(jù)函數(shù)的輸入推斷函數(shù)的執(zhí)行結(jié)果。這也就是Ken所謂的:
You can tell what it does without Looking at surrounding context. |
Ken在演講中給出了一個(gè)案例:
- def parseLocation(str: String): Location = {
- val parts = str.split(",")
- val secondStr = parts(1)
- val parts2 = secondStr.split(" ")
- Location(parts(0), parts2(0), parts(1).toInt)}
仔細(xì)閱讀這段代碼,你會(huì)發(fā)現(xiàn)這段代碼是不健壯的,可能存在如下錯(cuò)誤:
- 作為input的str可能為null
- parts(0)和parts(1)可能導(dǎo)致索引越界
- parts2(0)可能導(dǎo)致索引越界
- parts(1)未必是整數(shù),調(diào)用toInt可能導(dǎo)致類型轉(zhuǎn)換異常
這段代碼隱含的錯(cuò)誤還可能被廣泛地蔓延到系統(tǒng)的其他地方,只要該函數(shù)被調(diào)用。這種蔓延可能會(huì)因?yàn)楦嗲短椎恼{(diào)用而產(chǎn)生級(jí)聯(lián)的錯(cuò)誤效應(yīng)。例如:
- def doSomethingElse(): Unit = {
- // ...Do other stuff
- parseLocation("Melbourne, VIC 3000")}
而doSomethingElse()函數(shù)又被其他函數(shù)調(diào)用,這些潛在的缺陷會(huì)分布到各個(gè)直接或間接的調(diào)用點(diǎn)。這意味著代碼會(huì)繼承它所調(diào)用代碼的錯(cuò)誤以及副作用,使得對(duì)代碼功能的推理(reasoning)變得近乎不可能,更不用說代碼的模塊化(modularity)了。
我們當(dāng)然可以通過對(duì)null進(jìn)行檢測來避免出現(xiàn)這些錯(cuò)誤。然而看看各種出現(xiàn)null值的可能分支,需要我們做各種條件判斷,想象這樣的代碼都讓人不寒而栗。引入Option類型就可以很好地封裝這種可能性。按照Ken的說法就是:
All possibilities have been elevated into the type system. |
- def parseLocation(str: String): Option[Location] = {
- val parts = str.split(",")
- for {
- locality <- parts.optGet(0)
- theRestStr <- parts.optGet(1)
- theRest = theRestStr.split(" ")
- subdivision <- theRest.optGet(0)
- postcodeStr <- theRest.optGet(1)
- postcode <- postcodeStr.optToInt
- } yield Location(locality, subdivision, postcode)}
以上代碼中,split()函數(shù)返回的類型為Array[String],該類型自身是沒有optGet()函數(shù)的。但是我們可以為Array[String]定義隱式轉(zhuǎn)換:
- implicit class StringArrayWrapper(array: Array[String]) {
- def optGet(index:Int): Option[String] = {
- if (array.length > index) Some(array(index)) else None
- }}
optToInt方法可以如法炮制。
Ken的解決方案并沒有考慮到parseLocation函數(shù)入?yún)tr存在null值的可能,故而在對(duì)str調(diào)用split方法時(shí)仍然有可能導(dǎo)致拋出空指針異常。因此進(jìn)一步,我們還可以修改parseLocation函數(shù)的定義:
- def parseLocation(optStr: Option[String]): Option[Location]
顯然,通過引入Option,既規(guī)避了前面分析可能出現(xiàn)的錯(cuò)誤,又能避免編寫繁瑣的if判斷。這里的關(guān)鍵點(diǎn)是Option對(duì)兩種可能性(None與Some)的封裝。它由兩個(gè)代數(shù)類型Some與None構(gòu)成,前者包含了一個(gè)值,而后者則包含了一個(gè)不存在的值。事實(shí)上,Option是一個(gè)Maybe Monad,實(shí)現(xiàn)了flatMap與filter,因而在Scala中可以用for comprehension來訪問。
【本文為51CTO專欄作者“張逸”原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)聯(lián)系原作者】