[Scala] Use Either to hand error cases

在OOP的語言中,我們exception來表達程式錯誤或是系統crash.Exception又可以分為Checked ExceptionUnChecked Excpetion

  • Checked Exception: 會將要丟出的excpetion寫在function的signature.
    例如:

    1
    interface Foo() throws XXXException, YYYException
  • UnChecked Excpetion: 要丟出的excpetion不會寫在function的signature,呼叫function的人需要知道有exception要處理.

Checked Exception會違反Open/Closed Principle,當有新增丟出的excpetion,需要修改interface signature;UnChecked Excpetion則是需要知道有哪些exception需要處理.

這兩種exception孰好孰壞,在 Clean code: a handbook of agile software craftsmanship, Robert C. Martin 有提到:

1
2
3
4
5
6
The debate is over. For years Java programmers have debated over the benefits and liabilities of checked exceptions. When checked exceptions were introduced in the first version of Java, they seemed like a great idea. The signature of every method would list all of the exceptions that it could pass to its caller. Moreover, these exceptions were part of the type
of the method. Your code literally wouldn't compile if the signature didn't match what your code could do.

At the time, we thought that checked exceptions were a great idea; and yes, they can yield some benefit. However, it is clear now that they aren't necessary for the production of robust software. C# doesn't have checked exceptions, and despite valiant attempts, C++ doesn't either. Neither do Python or Ruby. Yet it is possible to write robust software in all of these languages. Because that is the case, we have to decide—really—whether checked exceptions are worth their price.

Checked exceptions can sometimes be useful if you are writing a critical library: You must catch them. But in general application development the dependency costs outweigh the benefits

在Java中建議使用UnChecked Excpetion,但是這會導致另外一個災難…

  1. 呼叫者不知道會丟出exception
  2. 呼叫者不知道有哪些excpetion要處理,當call chain很深的時候更是糟糕…
  3. 到處充滿了 try{...} catch{...}

相信使用Java開發的工程師應該感觸良多,尤其是在維護舊專案.一個call chain長達十幾層,每一層丟的excpetion都不一樣,有些層會處理exception,有些又不會,最後的大絕招就是每一層都加 try{...} catch{...}

例外處理在OOP是一個很重要的議題,沒有謹慎處理很容易造成維護困難和發生問題不容易找到問題點.
因為Exception是有side effect,在FP是不被准許的.所以在pure FP程式中是看不到excpetion和 try{...} catch{...}

Either

我們可以透過 Either 來達成,Left 放錯誤的物件,Right 放正確的物件:

1
2
3
4
5
6
7
8
9
sealed abstract class Either[A, B]
final case class Left[+A, +B](a: A) extends Either[A, B]
final case class Right[+A, +B](b: B) extends Either[A, B]

// for exmple
type Error = String

val result = Right[Error, Int](123)
val error = Left[Error, Int]("Something wrong!!!")

我們可以把正確的結果或錯誤放到 Either 這個容器,呼叫者可以清楚知道有需要處理錯誤情況,再來程式碼不再到處充斥 try{...} catch{...}

我們使用四則運算來當範例,如何使用 Either

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def div(a: Double, b: Double): Either[String, Double] = {
if(b == 0) Left[String, Double]("Can't divide by 0 !!!")
else Right[String, Double](a / b)
}

def add(a: Double, b: Double): Either[String, Double] = {
Right[String, Double](a + b)
}

def minus(a: Double, b: Double): Either[String, Double] = {
Right[String, Double](a - b)
}

def mul(a: Double, b: Double): Either[String, Double] = {
Right[String, Double](a * b)
}

// ((((1 + 2) * 3) / 4) - 5)

val result = for {
r1 <- add(1, 2)
r2 <- mul(r1, 3)
r3 <- div(r2, 4)
r4 <- minus(r3, 5)
} yield {
r4
}

// result: scala.util.Either[String,Double] = Right(-2.75)

// ((((1 + 2) * 3) / 0) - 5)

val result = for {
r1 <- add(1, 2)
r2 <- mul(r1, 3)
r3 <- div(r2, 0)
r4 <- minus(r3, 5)
} yield {
r4
}

// result: scala.util.Either[String,Double] = Left(Can't divide by 0 !!!)

根據上面簡單的範例,這樣的寫法很明顯優於傳統OOP用excpetion來處理錯誤:

  • 不再有 try{...} catch{...} 充斥在每個地方.
  • 呼叫者可以根據fucntion signature知道是否會產生錯誤.
  • 更可以專心在商業邏輯,而不用花多餘的心力在處理例外.

Try

1
2
3
4
5
sealed abstract class Try[+T]

final case class Failure[+T](exception: Throwable) extends Try[T]

final case class Success[+T](value: T) extends Try[T]

Try 也是類似於 Either 的context,有興趣的讀者可以試用看看.在實務上大部分都採用 Either,不採用 Try的原因是:

  • 沒有辦法對exception做 exhaustive pattern match,當是Failure的時候,呼叫者不知道有哪些exception會丟出,只知道會丟出一個 Throwable class

使用 Either 我們可以自訂Error type,當失敗時候就可以對Error type做 exhaustive pattern match,依據不同的錯誤做不同的處理.