也許你用的 function 在很多地方都用到,改變原 function 的定義影響甚大,也或者該 function 是寫在別的 module 裡,這時候你想用 Option 的話怎麼辦呢?
這時候我們可以 Lift (抬起來) 這些原始 function,並讓它們支持 Option 操作,請看以下程式,
def lift[A, B](f: A => B): Option[A] => Option[B] = _ map f
Option 的 map 就是用 function f 改變其值,然後在回傳 Option,而 lift 就是在多一層加工,把 f 包裝成 partial function 後回傳,
在 Scala 中,
_ map f
等於_.map(f)
,it's syntactic sugar 😄
以下程式我們把原本不是回傳 Option 的開根號 function,搖身一變成回傳 Option 的開根號 function 了。
val sqrtOpt: Option[Double] => Option[Double] = lift(math.sqrt)
sqrtOpt(Some(4))
如果原生 function 是需要傳入 2 個參數的呢?例如 max
function,
def max(x: Int, y: Int): Int
然後我們想要這樣使用,找到比較大的年紀,
def maxAge(age1: String, age2: String): Option[Int] =
val age1Opt = Try(age1.toInt)
val age2Opt = Try(age2.toInt)
math.max(age1Opt, age2Opt) // 會發生編譯錯誤
def Try[A](a: => A): Option[A] =
try
Some(a)
catch
case _: Exception => None
這裡的
Try
可視為一個萬用 function,幫助我們在有錯誤時回傳 None,實際上在 Scala 原生庫中已經有類似的 Try 資料型別來把 try-catch 包裹成 Success 和 Failure。
此時我們可以使用 lift 方式,設計一個 function map2
來讓 max 支援 Option,
def map2[A, B, C](a: Option[A], b: Option[B])(f: (A, B) => C): Option[C] =
a.flatMap(aa => b.map(bb => f(aa, bb)))
flatMap 的 high-order function 要回傳的東西是 Option,所以我們就把 b
當做這個 Option 回傳,但我們的目的是調用 f
,所以我們調用 b
Option 的 map,在把裡頭的值傳入 f
取得結果,
如此我們就可以讓 math.max
支持 Option 型別了。
def maxAge(age1: String, age2: String): Option[Int] =
val age1Opt = Try(age1.toInt)
val age2Opt = Try(age2.toInt)
map2(age1Opt, age2Opt)(math.max)
因為 lifting 這種功能的 function 在 Scala 很常見,所以 Scala 有提供一種叫 for-comprehensions 的語法,使其能自動擴展成 flatMap 和 map 呼叫,所以我們就能把 map2
改成下面這個樣子,
def map2_1[A, B, C](a: Option[A], b: Option[B])(f: (A, B) => C): Option[C] =
for {
aa <- a
bb <- b
} yield f(aa, bb)
看起來比較直觀了,Scala 會把 yeild
前的綁定 bb <- b
當作 map 呼叫,而其它都是使用 flatMap 呼叫。
Exception 的拋出會違反 Referential Transparency,所以 functional programming 處理錯誤的方式就是將錯誤視為某一種應該要返回的回傳值,其概念有點像 結構化程式語言 回傳錯誤碼那樣,但 FP 多包裹了一層,使其我們可以從這之取得正常值和錯誤值,所以我們設計了 Option
、 Either
等型別來使其符合 RT;
在來就是介紹了如何在不改寫既有 function 定義的情況下,使其支援 Option
或 Either
型別,最後就是如何使用 Scala 的 for-comprehensions 改寫 flatMap 和 map 的呼叫,讓我們看的順眼一些。