Scalaでリレーショナルデータベースを扱う場合、関連をたどっていく中でどのタイミングで情報が取得できなかったのかを返さねばならないことがあります。
Noneを盲目的に処理するのであれば、flatMapやfor式をつかえば畳み込んでスッキリかけるのですが、関連を取得していくなかでどのタイミングでNoneが取得されてしまったのか返したい場合にはそうは行かず、結局match caseの深いネストになってしまいます。
例を挙げます。
ユーザーとアドレスがそれぞれデータベースに格納されており、ユーザーIDを利用してそのユーザーを検索し、ユーザーが持つアドレスIDでアドレスを検索し、さらにその郵便番号を取得するような場合を考えます。
失敗結果としては
- ユーザーがみつからない
- ユーザーがアドレスを持っていない
- アドレスがみつからない
- アドレスが郵便番号を持っていない
という4つの失敗パターンがあり、それらを結果オブジェクトとして返さなくてはなりません。
以下のようなコードになります。
object MainBefore {
case class Address(id: Int, name: String, postalCode: Option[String])
case class User(id: Int, name: String, addressId: Option[Int])
val userDatabase: Map[Int, User] = Map (
1 -> User(1, "太郎", Some(1)),
2 -> User(2, "二郎", Some(2)),
3 -> User(3, "プー太郎", None)
)
val addressDatabase: Map[Int, Address] = Map (
1 -> Address(1, "渋谷", Some("150-0002")),
2 -> Address(2, "国際宇宙ステーション", None)
)
sealed abstract class PostalCodeResult
case class Success(postalCode: String) extends PostalCodeResult
abstract class Failure extends PostalCodeResult
case class UserNotFound() extends Failure
case class UserNotHasAddress() extends Failure
case class AddressNotFound() extends Failure
case class AddressNotHasPostalCode() extends Failure
// どこでNoneが生じたか取得しようとするとfor式がつかえず地獄のようなネストになる
def getPostalCodeResult(userId: Int): PostalCodeResult = {
findUser(userId) match {
case Some(user) =>
user.addressId match {
case Some(addressId) =>
findAddress(addressId) match {
case Some(address) =>
address.postalCode match {
case Some(postalCode) => Success(postalCode)
case None => AddressNotHasPostalCode()
}
case None => AddressNotFound()
}
case None => UserNotHasAddress()
}
case None => UserNotFound()
}
}
def findUser(userId: Int): Option[User] = {
userDatabase.get(userId)
}
def findAddress(addressId: Int): Option[Address] = {
addressDatabase.get(addressId)
}
def main(args: Array[String]): Unit = {
Console.println(getPostalCodeResult(1)) // Success(150-0002)
Console.println(getPostalCodeResult(2)) // AddressNotHasPostalCode()
Console.println(getPostalCodeResult(3)) // UserNotHasAddress()
Console.println(getPostalCodeResult(4)) // UserNotFound()
}
}
getPostalCodeResultが鬼のようなmatch caseのネストになっていることがわかります。このような可読性の低いコードを、Eitherを使って書きなおすことができます。
以下のように全てのfindメソッドをEitherでFailureをLeftに、正常取得できた場合の値の型をRightにして書き直します。
findの各段階でFailureオブジェクトに引き換えるという動きをさせるわけです。
リファクタリングした結果は以下のようになります。
object MainRefactored {
case class Address(id: Int, name: String, postalCode: Option[String])
case class User(id: Int, name: String, addressId: Option[Int])
val userDatabase: Map[Int, User] = Map (
1 -> User(1, "太郎", Some(1)),
2 -> User(2, "二郎", Some(2)),
3 -> User(3, "プー太郎", None)
)
val addressDatabase: Map[Int, Address] = Map (
1 -> Address(1, "渋谷", Some("150-0002")),
2 -> Address(2, "国際宇宙ステーション", None)
)
sealed abstract class PostalCodeResult
case class Success(postalCode: String) extends PostalCodeResult
abstract class Failure extends PostalCodeResult
case class UserNotFound() extends Failure
case class UserNotHasAddress() extends Failure
case class AddressNotFound() extends Failure
case class AddressNotHasPostalCode() extends Failure
// 本質的に何をしているかわかりやすくリファクタリング
def getPostalCodeResult(userId: Int): PostalCodeResult = {
(for {
user <- findUser(userId).right
address <- findAddress(user).right
postalCode <- findPostalCode(address).right
} yield Success(postalCode)).merge
}
def findUser(userId: Int): Either[Failure, User] = {
userDatabase.get(userId).toRight(UserNotFound())
}
def findAddress(user: User): Either[Failure, Address] = {
for {
addressId <- user.addressId.toRight(UserNotHasAddress()).right
address <- addressDatabase.get(addressId).toRight(AddressNotFound()).right
} yield address
}
def findPostalCode(address: Address): Either[Failure, String] = {
address.postalCode.toRight(AddressNotHasPostalCode())
}
def main(args: Array[String]): Unit = {
Console.println(getPostalCodeResult(1)) // Success(150-0002)
Console.println(getPostalCodeResult(2)) // AddressNotHasPostalCode()
Console.println(getPostalCodeResult(3)) // UserNotHasAddress()
Console.println(getPostalCodeResult(4)) // UserNotFound()
}
}
以上のようになり、
def getPostalCodeResult(userId: Int): PostalCodeResult = {
(for {
user <- findUser(userId).right
address <- findAddress(user).right
postalCode <- findPostalCode(address).right
} yield Success(postalCode)).merge
}
getPostalCodeResultが本質的に何をしているのかが非常にわかりやすいコードとなりました。何をしているかというと、Eitherではfor式を直接つかえないので.rightというメソッドで、RightProjectionという型にして、for式が利用できる形に変換しています。そのあと、margeメソッドにより中身を畳み込んで取得しています。
@xuwei_kさんからの指摘を頂いたので、リファクタ後もmatch caseで切り出したメソッド内でEitherにしていた部分をtoRightを使って表現するように修正しました。
なおこの内容は、
https://gist.github.com/rirakkumya/2382341
で元々議論されていたものをわかりやすく書きなおしたものになります。
ぜひ今後このようなリファクタリングにチャレンジしてみてください。
コード自体は、 https://github.com/sifue/right_projection にありますので、sbt run
などして動きを試してみてください。