說到 Option[T] 啊
上一篇我們已經看到,在 Scala 裡中我們比較喜歡用 Option[T] 這個東西,來做為有可能失敗的函式的傳回值。
所以如果我們要寫一個「安全版本」的 Integer.parseInt(),而且我們想要限制他的處理範圍只有 0 到 100 的話,我們可能會寫出下面這樣的東西:
def convertToInt(str: String): Option[Int] =
{
try {
val resultInt = str.toInt // 等同 Integer.parseInt(str)
// 值域檢查
if (resultInt < 0 || resultInt > 100) {
throw new Exception("不在範圍內")
}
Some(resultInt)
} catch {
case e: Exception => None
}
}
在上面的例子中,如果使用者傳入的字串可以被轉換成為整數,那麼就會返回 Some[Int],而這個 Some[Int] 裡面會放入轉換過後的整數,但如果使用者給的是無法轉換的字串,又或者值域超過範圍,那就會返回 None 告訴使用者,無法正確轉換。
scala> convertToInt("53")
res3: Option[Int] = Some(53)
scala> convertToInt("NotANumber")
res4: Option[Int] = None
scala> convertToInt("-100")
res5: Option[Int] = None
scala> convertToInt("150")
res6: Option[Int] = None
接著,我們可以用上一篇講到的各式各樣的方法,來處理我們取得的這個 Option[Int],就看你習慣哪一種做法。
改用 Either[A, B] 來回傳狀態
上面使用 Option[Int] 的作法,確實可以達到「區別轉換成功和失敗」的效果,但也有一些不完美--當這個函式返回 None 時,我們只能知道轉換失敗了,但卻不知道失敗的原因為使用者失入了不合法的字串,又或者字串沒問題,只是值域超過了而已。
當然,在 Java 裡面,我們可能會自己訂一個 Exception 然後直接丟出,讓呼叫函式的人自己去 catch,但我們用 Option[T] 這類的東西,就是因為不想要丟 Exception 啊,有沒有方法可以不丟 Exception,但又讓呼叫函式的人知道出了什麼問題呢?
答案是有的--Scala 中有一個 Either[A, B] 的類別,這個類別就是專門來做這種事的。
這個類別和 Option[T] 很像,但不同的是,他的兩個子類別 Left[A] 和 Right[B],都可以放東西,如果要對比的話大概是這樣:
- Option[T] <--> Either[A, B]
- None <--> Left[A]
- Some[T] <--> Right[B]
要注意的是,在使用 Either[A, B] 的時候,我們習慣上把 Left[A] 當做失敗時的回傳值,而把 Right[B] 當做成功時的回傳值。
假設我們現在要讓使用者知道到底發生了什麼錯,我們就可以將 convertToInt 宣告成返回 Either[Exception, Int] 這個資料型態,然後在真的成功的時候返回 Right[Int],失敗的時候返回 Left[Exception]。
// 定義我們自己的 Exception
class NotInRangeException extends Exception("不在範圍內")
def convertToInt(str: String): Either[Exception, Int] =
{
try {
val resultInt = str.toInt // 等同 Integer.parseInt(str)
// 值域檢查
if (resultInt < 0 || resultInt > 100) {
throw new NotInRangeException
}
Right(resultInt)
} catch {
case e: Exception => Left(e)
}
}
當我們改寫成這樣之後,呼叫 convertToInt 時如果發現錯誤,那就會返回一個 Left[Exception],並且帶有 Exception 物件在裡面:
scala> convertToInt("33")
res15: Either[Exception,Int] = Right(33)
scala> convertToInt("AAA")
res12: Either[Exception,Int] = Left(java.lang.NumberFormatException: For input string: "AAA")
scala> convertToInt("150")
res13: Either[Exception,Int] = Left(NotInRangeException: 不在範圍內)
scala> convertToInt("-100")
res14: Either[Exception,Int] = Left(NotInRangeException: 不在範圍內)
Java 式的處理方法
當然,當我們拿到了一個 Either[Exception, Int],還是要針對他是 Left[Exception] 來做錯誤處理,或者當他是 Right[Int] 的時候,把裡面的整數值拿出來做一些事情。
和 Option[T] 一樣,我們可以用 Java 裡習慣的 if / else 判別式來針對不同的情況做不同的事情:
val result = convertToInt("...")
if (result.isLeft) {
// 失敗的時候要做的事
val exception: Exception = result.left.get
println("失敗:" + exception)
} else if (result.isRight) {
// 成功的時候要做的事
val value: Int = result.right.get
println("成功:" + value)
}
當然,我們已經知道在大部份的情況下,我們會覺得在 Scala 裡用這種寫法會讓人感覺程式碼很醜。
Pattern Matching 也是可以的啦
因為上面用 if / else 的程式碼實在有點醜,所以我們還是換另一個比較有 Scala 的風味,叫做 Pattern Matching 的工具來處理好了:
val result = convertToInt("...")
result match {
case Left(exception) => println("失敗:" + exception)
case Right(value) => println("成功:" + value)
}
這下程式碼看起來清爽多了,而且也少了一個 get 的動作。
當然用 map 和 foreach 也是可以的
在 Option 的那篇裡看到了,有的時候比起 Pattern Matching,我們會比較喜歡用 map / foreach 這種方式來思考和解決問題,而這在 Either[A, B] 上也是沒問題的。
要在 Either[A, B] 上使用 map / foreach 的話,首先要使用 .right 或 .left 來拿到一個叫 LeftProjection / RightProjection 的東西,這個東西有一個特性--如果該 Either[A, B] 並不是該類別,那什麼事也不會發生。
這個概念不太好理解,所以我們還是直接來看程式碼:
scala> // x 是 Left[Int]
scala> val x: Either[Int, String] = Left(10)
x: Either[Int,String] = Left(10)
scala> // y 是 Right[Int]
scala> val y: Either[Int, String] = Right("String")
y: Either[Int,String] = Right(String)
scala> // 因為 x 確實是 Left,所以 map 正常運作,把 Left(10) 變成 Left(20)
scala> x.left.map(_ + 10)
res0: Product with Serializable with Either[Int,String] = Left(20)
scala> // 因為 y 不是 Left,所以什麼都不做,維持 Right(String)
scala> y.left.map(_ + 10)
res1: Product with Serializable with Either[Int,String] = Right(String)
scala> // 因為 x 確實是 Left,所以把 Left 裡的值(10) 走訪一遍後印出來
scala> x.left.foreach(println _)
10
res3: Any = ()
scala> // 因為 y 不是 Left,所以什麼事都沒發生
scala> y.left.foreach(println _)
res4: Any = ()
scala> // 因為 x 不是 Right,所以什麼事都沒發生
scala> x.right.foreach(println _)
res5: Any = ()
scala> // 因為 y 確實是 Right,所以走訪後印出來
scala> y.right.foreach(println _)
String
res6: Any = ()
你要用 for 迴圈也是可以的啦!
我們之前在 Option[T] 的部份看過,我們可以用 for 來操作 Option[T],當然這在 Either[A, B] 上也是可以的,只是你要先指定要處理的是什麼東西。
舉例來講,你可以這樣寫:
val either1: Either[String, Int] = Right(10)
val either2: Either[String, Int] = Left("Error")
// 只有 either1 是 Left 的時候才會執行
for (error <- either1.left) {
println(error)
}
// 只有 either1 是 Right 的時候才會執行
for (content <- either1.right) {
println(content)
}
// 只有 either1 和 either2 都是 Right 的時候才會執行
for (content1 <- either1.right;
content2 <- either2.right) {
println(content1)
println(content2)
}
所以基本上,Option[T] 能用的東西 Either[A, B] 也都能用,而且讓你可以多一個欄位來儲存錯誤訊息之類的。
當然,Either[A, B] 不是只能用在回傳錯誤訊息,由於 Either 裡的 A 和 B 可以是任意的型別,所以也常用在一個函式或方法需要回傳兩種可能的資料型態的情境中。
回響