Scalaではエラー処理に例外ではなくEitherを使うのが良いという話を聞いたのですが、実用ケースがうまく想像できなかったので、書いてみました。
※ あくまで私の考えた結果なので、ベストプラクティスであるかはわかりません。
参考
下記資料を参考にさせていただきました。
https://scala-text.github.io/scala_text/error-handling.html
例外処理について
一般にエラーが起きたときの挙動を制御するときは、例外をthrowすることが一般的だと思います。しかし、例外には下記のような問題点があります。
- 処理の流れがわかりづらい
- 非同期プログラミングで扱いにくい
- 型チェックが動作しない
そのため、Scalaでは例外を投げるのではなく、メソッドの返り値としてエラーを表現することが推奨されています。
Eihterとは
エラーを表現することができる型の一つです。
LeftかRightいずれかの値をとるような構造になっており、エラー処理で利用する場合は、Leftにエラー内容、Rightに正常時の返り値を格納するようにして使います。
map, flatmap, forなどを利用することにより、Left, Rightどちらであっても適用できるような処理を書くことができるので、メインの処理から、エラー対応のロジックを切り離すことができます。
Eitherによるエラー処理の例
具体的なユースケースを見るために、
下記の処理フローを備えたプログラムを想定します。
- データを読み込む
- データを加工する
- データを書き込む
各ステップにおいて、読み込みエラー、加工エラー、書き込みエラーが想定されます。
以下、Eitherを利用したエラー処理の例となります。
(具体の処理メソッドの記述は省略しています)
まず、各処理のエラー型を定義します。
sealed trait ProgramError
case object ReadDataError extends ProgramError
case object ProcessDataError extends ProgramError
case object WriteDataError extends ProgramError
各処理からEither型の値を返すインターフェースを作成します。
def readData(read: () => String): Either[ProgramError, String] = {
"""
read: データ読み込み用の関数
"""
try {
Right(read())
} catch {
case e: Exception => {
Left(ReadDataError)
}
}
}
def ProcessData(process: () => String): Either[ProgramError, String] = {
"""
process: データ加工用の関数
"""
try {
Right(process())
} catch {
case e: Exception => {
Left(ProcessDataError)
}
}
}
def writeData(write: () => Unit): Either[ProgramError, Unit] = {
"""
write: データ書き込み用の関数
"""
try {
Right(write())
} catch {
case e: Exception => {
Left(WriteDataError)
}
}
}
具体の処理プログラムは下記のようになります。
val inputData: Either[ProgramError, String] = readData(
readSomething()
)
val processdData: Either[ProgramError, String] = inputData.fold(
l => Left(l),
r => processData(processSomething(r))
)
val result: Either[ProgramError, Unit] = processdData.fold(
l => Left(l),
r => writeData(writeSomething(r))
)
// 発生したエラーに対応したメッセージを出力する。
result match {
case Left(ReadDataError) => System.err.println("ReadDataError occured")
case Left(ProcessDataError) => System.err.println("ProcessDataError occured")
case Left(WriteDataError) => System.err.println("WriteDataError occured")
}
foldを利用することで、値がRight, Leftそれぞれの場合で処理を分岐させることができます。
Eihterの型を保ったまま、処理を続けていることがわかるでしょうか。
このような形式にすることで、各処理の途中でエラー対応を実施するのではなく、最後にまとめて扱うことができるようになります。