我們在 Day 2 - 什麼是 Funcational Programming? 有提到拋出 exception 是某種 side effect ,倘若 exception 不能用,那我們該拿什麼來替代?我們該怎麼用 functionally 的方式處理它們呢?
方法就是在 function 的回傳中,把 錯誤 跟 正常 一起表示,然後也是用 pattern match 和 high-order function 來應對錯誤情況下的操作,也就是說,錯誤 不是例外,而是回傳值之一,有點像 結構化程式語言 回傳錯誤碼那樣,但多了更多東西,這 3 天就是要來介紹這些,就讓我們開始吧。
首先我們來看一段程式,
def failingFn(i: Int): Int =
val y: Int = throw new Exception("fail")
try {
val x = 42 + 5
x + y
}
catch {
case e: Exception => 43
}
若我們在 Scala REPL 中呼叫 failingFn
function,會得到下面結果,
scala> failingFn(5)
java.lang.Exception: fail
at rs$line$1$.failingFn(rs$line$1:2)
... 35 elided
如同在 Day 2 提到的,要驗證 y 是否符合 RT 的方法就是使用 Substitution Model 把用到它的地方替換掉,
def failingFn2(i: Int): Int =
try {
val x = 42 + 5
x + ((throw new Exception("fail")): Int)
}
catch {
case e: Exception => 43
}
相同的調用但卻得到了不同的結果;
scala> failingFn2(5)
val res1: Int = 43
這裡我們可以觀察到 2 個主要問題:
exception 破壞了 RT 且在某些情況下回傳的值會依情境不同而有所差異,當 catch 區塊處理多種 exception 時,可能會得到不同結果,我們不應該使用 try-catch 來控制流程。
exception 不是型別安全 (type-safe) 的,failingFn, Int => Int
是給它 Int 然後回傳 Int,我們並不知道要處理 exception。
若你熟悉 Java,你或許會想 Java 不是有提供一個功能叫 Checked Exception 嗎?這個不是可以解決上面的問題 2 了,這沒錯,但這個不作用在 high-order function 上,
例如 List 的 萬用 map function,我們不可能在這裡檢查所有在
f
中可能會拋出的所有 exception,所以即使在 Java 也是用RuntimeException
來表達這種錯誤。def map[A, B](l: List[A], f: A => B): List[B]
難道就沒有一種替代方案能避免上述問題,卻同時不失去 Exception 帶給我們有關整合和中心化錯誤邏輯處理的好處嗎?
先來看另一個例子,當輸入是空陣列時拋出 exception,
def mean(xs: Seq[Double]): Double =
if xs.isEmpty then
throw new ArithmeticException("mean of empty list")
else
xs.sum / xs.length
Seq
是 Scala 中所有有連續性的集合資料結構的抽象類別,包含有索引的 Array 或者線性的 Linked List 的父類別都是 Seq,一些共用的好用工具 function 在 Seq 中都有,所以通常都用 Seq 來當做參數型態,Scala 中相關資料結構可參考此 文件。
這個 function 一般被稱做 partial
function,因為它對輸入做出了輸入類型未暗示的假設,此處的假設是 Seq 非空,
感謝邦友 shootingstar 的補充。
如果不想無腦拋出 exception,或許我們就直接計算!?
def mean1(xs: Seq[Double]): Double =
xs.sum / xs.length
因為型態是 Double,當除數為 0.0 時,其回傳值會是 Double.NaN
,但這也會有其他的問題,
首先是調用者要自己在腦中記得,我要多加一個 if 判斷 mean 的結果是不是 NaN,其次是這種做法不泛用,無法抽象成萬用的多型方法,像 map
那樣,且若是多型方法你也是不曉得要回傳 Double 型態的 NaN,還是非基本型態的 null。
或許我們有第二種作法,多加一個參數給定若 List 長度為 0 時會回傳的初始值,像這樣,
def mean2(xs: Seq[Double], onEmpty: Double): Double =
if xs.isEmpty then
onEmpty
else
xs.sum / xs.length
然後就又有人有問題了,如果我想在遇到空 List 直接中斷程式怎麼辦?你這樣改我要調整的地方很多耶!
看起來這種做法還是不夠自由,所以我們需要一個方法去應對以上所有遇到的情況。
enum Option[+A]:
case Some(get: A)
case None
enum Either[+E, +A]:
case Left(get: E)
case Right(get: A)
我們一樣會透過實作 Scala 基本庫已有的類別 Either
和 Option
來當做練習,了解 functional programming 中如何處理錯誤,也能符合 Referential Transparency。
然後明天繼續。
可以考慮把拋出例外和捕捉例外分成兩件事來看待,假如你的function只有依賴input但有可能拋出例外的話,這個function仍舊是pure function,但我們會稱它為partial function。
拋出例外、無窮迴圈、無限遞迴等情況我們會稱它作bottom
(一般會寫成⊥
),⊥
並不會讓pure function變成impure,拿除法function來當例子便不難理解,只有分母傳入0的時候function才會給你⊥
,其他狀況都是正常的數字,並未違反Referential Transparency。
反之,function內如果有捕捉例外就真的有side effect,因為這個function的input便已不只依賴傳進來的參數,還多了一個不知從何處而來的例外,function的狀態頓時變得無法控制,所以你上面的failingFn
才會得到它是impure的結論。
感謝補充分享,已調整成較為正確的用詞。