Scala の Option, Either とエラー処理
目次
Scala ではエラー処理に使えるクラス・仕組みが沢山ありますが、今回は Option, Either を使った方法を色々紹介します。
前提知識として、Scala の Option, Either を触ったことがあり、基本的な Scala の文法を理解しているものとします。
共通で使うコード
本題に入る前に、コード例で共通で使うクラスなどを定義します。
基本的な処理はこの辺を使います。
// ユーザー情報
class User {
def id: Int
def getFather: Option[User]
def getEmailAddress: Option[String]
// Error型は後の方で定義しています
def getEmailAddressEither: Either[Error, String]
}
// ユーザー情報を取得する(Option版)
object UserRepository {
def getUserById(id: Int): Option[User]
}
// ユーザー情報を取得する(Either版)
object UserRepositoryEither {
def getUserById(id: Int): Either[Error, User]
}
エラー内容などを表す以下のようなクラスも定義します。
object HttpStatus {
val Ok = 200
val NotFound = 404
val BadRequest = 403
val InternalServerError = 500
}
// 独自エラーの基底クラス
trait Error {
val internalErrorCode: Int
val httpResponse: Int
def writeToLog: Unit
}
// ユーザーが存在しない場合のエラー
object UserNotFoundError extends Error {
override val internalErrorCode: Int = 1
override val httpResponse: Int = HttpStatus.NotFound
override def writeToLog: Unit = ???
}
// メアドが無い人にメールを送ろうとした場合のエラー
object EmailingUserWithNoEmailAddressError extends Error {
override val internalErrorCode: Int = 2
override val httpResponse: Int = HttpStatus.InternalServerError
override def writeToLog: Unit = ???
}
独自の例外も定義しておきます。
class UserNotFoundException(userId: Int) extends Exception(s"ユーザー $userId は存在しません")
class EmailingUserWithNoEmailAddressException(userId: Int)
extends Exception(s"メールアドレスを登録していないユーザー $userId にメールを送ろうとしました")
Option を使った例
パターンマッチを使う
これはみんな知っていると思いますので簡単に流しますが、以下のような例です。exampleOption1
の戻り値ですが、Ok
と NotFound
が共に Int
なので、全体として Int
型となります。
import HttpStatus._
def exampleOption1: Int = {
UserRepository.getUserById(1) match {
case Some(user) =>
doSomethingForUser(user)
Ok
case None =>
NotFound
}
}
パターンマッチを使った方法だと、以下のように複数の Option を扱う場合にネストが深くなってしまうのが難点です。
def exampleOption2: Int = {
UserRepository.getUserById(1) match {
case Some(user) =>
user.getFather match {
case Some(father) =>
doSomethingForUser(father)
Ok
case None =>
NotFound
}
case None =>
NotFound
}
}
次項以降で、もう少し綺麗に書く方法について考えていきます。
getOrElse を使う
Option
には getOrElse
というメソッドがありますが、それを使うと少しネストを浅く出来ます。少しはマシですが、処理がもう少し増えてくると、やはり綺麗じゃ無いなと思ってしまいます。
def exampleOption3: Int = {
UserRepository.getUserById(1).map { user =>
user.getFather.map { father =>
doSomethingForUser(father)
Ok
}.getOrElse(NotFound)
}.getOrElse(NotFound)
}
ちなみに x.getOrElse(y)
という形の場合、x
が None
だった場合のデフォルト値を y
として渡す事が多いと思います。例えば以下のようなパターンです。
val user: User = UserRepository.getUserById(1)
.getOrElse(new GuestUser)
doSomethingForUser(user)
ただ getOrElse
の中で例外を投げるというのも、よく使うパターンです。以下のような例です。この場合も、user
の型は User
となります。今回は初心者向けの記事なので、なぜそうなるかの理由は省略します。
val user: User = UserRepository.getUserById(1)
.getOrElse(throw new UserNotFoundException(1))
doSomethingForUser(user)
for を使った方法
もう少し綺麗に書きたい場合、for
が使えます。
Scala の for
は、実態は withFilter
, map
, flatMap
を組み合わせたものですが、これもここでは詳しく説明しません。
for を使った例としては以下の通りです。
ID=1の user
がいて、かつ、その user
の父親がいれば、その father
に対して処理をし Ok
を返します。一方、user
がいないか、その user
の父親がいない場合には NotFound
を返します。大分スッキリしました。
def exampleOption4: Int = {
(for {
user <- UserRepository.getUserById(1)
father <- user.getFather
} yield {
doSomethingForUser(father)
Ok
}).getOrElse(NotFound)
}
ただ、以下のような処理の場合には、for を使って書き換えることができません。ユーザーが存在しない場合とメアドが登録されていない場合で処理を変えたい場合です。
def exampleOption5: Int = {
UserRepository.getUserById(1) match {
case Some(user) =>
user.getEmailAddress match {
case Some(emailAddress) =>
sendEmail(emailAddress)
Ok
case None =>
logger.error(s"メールアドレスを登録していないユーザー ${user.id} にメールを送ろうとした")
InternalServerError
}
case None =>
NotFound
}
}
どうすれば良いのかは、いくつかの案を後の方で紹介します。
Option
に関してはここまでにして、次に Either
について書いていきます。
Either を使った例
パターンマッチを使う
まずは基本的な形から紹介します。これの難点は Option
を使った例と同様で、処理が増えるとネストが深くなる点です。
def exampleEither1 = {
UserRepositoryEither.getUserById(1) match {
case Left(error) =>
error.writeToLog
error.httpResponse
case Right(user) =>
user.getEmailAddressEither match {
case Left(error) =>
error.writeToLog
error.httpResponse
case Right(emailAddress) =>
sendEmail(emailAddress)
Ok
}
}
}
for を使うとスッキリ書ける
Either
の場合に for
を使うと、以下のようになります。
def exampleEither2: Int = {
(for {
user <- UserRepositoryEither.getUserById(1)
emailAddress <- user.getEmailAddressEither
} yield {
sendEmail(emailAddress)
Ok
}).left.map { err =>
err.writeToLog
err.httpResponse
}.merge
}
今までより少し複雑なので、以下に解説します。
まずは以下のブロックですが、Scala の Either
は(2.12以降で)right-biased となっていて、map
, flatMap
などは値が Right
だった場合に適用されます。つまり、以下の部分は、ユーザーが存在し、かつ、そのユーザーのメアドが登録されている場合、yield
の中が実行され、その戻り値( Ok
= Int
型)が Either
の Right
側になります。従って、全体の戻り値としては Either[Error, Int]
となります。
(for {
user <- UserRepositoryEither.getUserById(1)
emailAddress <- user.getEmailAddressEither
} yield {
sendEmail(emailAddress)
Ok
}) // Either[Error, Int]
次に以下の部分ですが、Either[Error, Int]
が Left
だった場合にこちらの処理が実行されます。エラーのログを出力し、Left
側は err.httpResponse
= Int
型に変換されま、merge
の前までの部分の型は Either[Int, Int]
となります。そして、最後の merge
で、Left
と Right
が合体して、全体として Int
型となります。
}).left.map { err =>
err.writeToLog
err.httpResponse
}.merge
Option を Either に変換する
上の方の exampleOption5
は、少し読みづらいコードで、for
文を使う事も出来ませんでしたが、Option
を Either
に変換することで、すぐ前に説明した exampleEither2
のように書くことが出来ます。
具体的には、以下のようにかけます。
def exampleEither3 = {
(for {
user <- UserRepository.getUserById(1).toRight(UserNotFoundError)
emailAddress <- user.getEmailAddress.toRight(EmailingUserWithNoEmailAddressError)
} yield {
sendEmail(emailAddress)
Ok
}).left.map { err =>
err.writeToLog
err.httpResponse
}.merge
}
Option
を Either
に変換するには、上に書いた通り toRight
メソッドを使います。toRight
に渡す値は、Either
の Left
側になります。
getOrElse を使う
Either
にも getOrElse
メソッドが存在します。前述の通り、 Either
は right-biased なので、値が Left
だった場合に getOrElse
の中身が使われます。
あまり無いかもしれませんが、Either
が Left
だった場合に適切な例外を投げたい場合などは、以下のように書けます。
def exampleEither4 = {
val user = UserRepositoryEither.getUserById(1).getOrElse(throw new UserNotFoundException(1))
val email = user.getEmailAddressEither.getOrElse(throw new EmailingUserWithNoEmailAddressException(1))
sendEmail(email)
Ok
}
まとめ
Scala の Option
は強力で、Scala を使っている人であれば日常的に使っていることと思います。一方、Either
も強力ですが、Option
とどう使い分けたら良いのかなどが分からず、Option
ほど使っていない人も多いと思います。
本記事では、 Option
, Either
を使ったエラー処理の方法を、色んなパターンで説明してきました。
for
をうまく使うOption
と独自の例外を使うEither
と独自のエラークラスを使う
といった方法で、エラー処理を読みやすくできる可能性があるので、自分のコードを一度見直してみてはどうでしょうか。
なお、Scala でエラー処理に使える仕組みとしては scala.util.Try
などもあるので、これに関しては機会があれば別途説明します。
コメントを残す