この記事はチームラボエンジニアリングアドベントカレンダー20日目の記事です。
ScalaにおけるOption型の在り方
ScalaはJavaの影響を受けた言語で、Javaの弱みを補うことができる仕組みがいくつか用意されています。その中に、「nullの代わりにOption型を使用できる」という仕組みがあるのですが、これを活用すればNullPointerException(よくヌルポと呼ばれていますね)を無くすことができます。一応Scalaでnullを扱うこともでき、Javaライブラリーを利用するときにnullを使うこともありますが、通常はScala言語の思想的にもOption型を使用する傾向にあります。
Option型とは?
JavaのOptional<T>と似ています。
ScalaのOption型にはサブクラスが2つあります。
- Some・・・値が存在することを表す型
- None・・・値が存在しないことを表す型
よくある使い方
前提
学校のケースクラスSchool、生徒のケースクラスStudentがあり、
DBから取得した値をそれぞれのケースクラスに詰めて返すメソッドがあることとします。
Schoolのidは値クラスSchoolIdを使用し、Studentのidは値クラスStudentIdを使用します。
// 学校
case class School(
id: SchoolId,
name: String
)
// 生徒
case class Student(
id: StudentId,
schoolId: SchoolId,
name: String
)
// 値クラスSchoolId
case class SchoolId(value: Long) extends AnyVal {
}
// 値クラスStudentId
case class StudentId(value: Long) extends AnyVal {
}
trait SchoolRepository {
// DBから学校のデータを取得してOption[School]を返すメソッド
def findSchool(id: SchoolId): Option[School] = {
// ここにDBからSchoolのデータを取得する処理が書かれていることとします。
}
}
trait StudentRepository {
// DBから生徒のデータを取得してOption[Student]を返すメソッド
def findStudent(id: StudentId): Option[Student] = {
// ここにDBからStudentのデータを取得する処理が書かれていることとします。
}
}
データの中身があるかどうかをBooleanで返したい
- isEmpty・・・Optionの中身がNone(データが存在しない)ならばtrue
val schoolId: SchoolId = SchoolId(1)
val schoolOpt: Option[School] = schoolRepository.findSchool(schoolId)
if (schoolOpt.isEmpty) {
println("schoolOptの中身はNoneです")
}
- isDefined・・・Optionの中身がSome(データが存在する)ならばtrue
val schoolId: SchoolId = SchoolId(1)
val schoolOpt: Option[School] = schoolRepository.findSchool(schoolId)
if (schoolOpt.isDefined) {
println("schoolOptの中身はSomeです")
}
データがSomeとNoneの場合それぞれで処理を分けたい
match式を使います。Scalaではよく使う書き方です。
val schoolId: SchoolId = SchoolId(1)
val schoolOpt: Option[School] = schoolRepository.findSchool(schoolId)
schoolOpt match {
case Some(school) => println(s"${schoolId}の学校の名前は${school.name}です") // Someにマッチした場合はschoolという名前で中身の値をとりだしている。
case None => println(s"${schoolId}の学校は存在しません")
}
Optionの中身に何か処理を施した上で中身を取り出したい
Option型にはmapメソッドが用意されているのでこれらを使います。mapを使うと、mapの処理内ではSomeとして扱われますが、戻り値はOptionです。下記の処理では、Someであれば生徒の名前にtoUpperCaseが適用されてから中身が取り出されます。
val studentId: StudentId = StudentId(1)
val studentOpt: Option[Student] = studentRepository.findStudent(studentId)
// 仮にstudentOptの中身がSomeだとして、生徒の名前はbobだったとする。
val studentName: String = studentOpt.map(s => s.name.toUpperCase).getOrElse(s"${studentId}の生徒は存在しません")
// studentOptの中身がSomeであれば"BOB"が出力される
println(s"${studentName}")
複数のOptionの存在確認をし、全てがSomeの場合のみ処理を実行したい
for式を使います。for式はいずれかの値がNoneだったらその時点で式が終了します。後述するfor-yield式と違って値を返しません。
複数のOptionを確認するときにmatch式を使うと、match式をネストする必要がありますが、for式、for-yield式を使うとネストがなくなりスッキリかけます。
val studentId: StudentId = StudentId(1)
for {
student <- studentRepository.findStudent(studentId) // ここでNoneだったらfor式が終了する
school <- schoolRepository.findSchool(student.schoolId) // 上記同様
} {
println(s"${studentId}の生徒の名前は${student.name}です。この生徒が在籍している学校名は${school.name}です") // 全てがSomeだった場合にこの処理が実行される
}
複数のOptionの存在確認をし、いずれかがNoneだったら別の処理を実行したい
for-yield式を使います。for-yieldはOption[T]を返します。下記の例では、全てSomeだった場合はSome[String]を返し、いずれかがNoneだった場合はNoneを返します
val studentId: StudentId = StudentId(1)
val messageOpt: Option[String] = for {
student <- studentRepository.findStudent(studentId) // Someだったら中身を取り出してstudentに代入する。NoneだったらmessageOptにNoneを返してfor-yield式は終了する
school <- schoolRepository.findSchool(student.schoolId) // 上記同様。この時点でstudentは存在することが確定している
} yield {
s"${studentId}の生徒の名前は${student.name}です。この生徒が在籍している学校名は${school.name}です" // 全てがSomeだったらSomeを返す
}
// studentとschoolのいずれもSomeの場合、messageOptにはyieldで返した値が入っており、"${studentId}の生徒の名前は${student.name}です。この生徒が在籍している学校名は${school.name}です"が出力される。
// studentもしくはschoolのいずれかがNoneの場合、messageOptはNoneになり、"${studentId}の生徒は存在しません"が出力される。
println(messageOpt.getOrElse(s"${studentId}の生徒は存在しません"))
SomeかNoneそれぞれで別の型を返したり、例外処理をしたい
Either型を使います。
Either型にも2つのサブクラスが存在します。
- Left・・・失敗した場合の型として扱います。下記の例の場合for-yield式内でLeft[String]を返しています。
- Right・・・成功した場合の型として扱います。下記の例の場合for-yield式内でRight[(Student, School)]を返しています。
val studentId: StudentId = StudentId(1)
val studentEither: Either[String, (Student, School)] = for {
student <- studentRepository.findStudent(studentId).toRight(s"${studentId}の生徒は存在しません") // Optionの中身がSomeだったらtoRightが成功する。Noneの場合は()内のStringでLeft[String]を返す
school <- schoolRepository.findSchool(student.schoolId).toRight(s"${schoolId}の学校は存在しません") // 上記同様
} yield {
(student, school) // 全てがRightだったらStudentとSchoolのタプルをRight型で返す
}
// Either型の値をmatch式で確認する
studentEither match {
case Left(value) => println(value)
case Right(value) => println(s"${studentId}の生徒の名前は${value._1.name}です。この生徒が在籍している学校名は${value._2.name}です")
}
最後に
Scalaの便利な仕組みのほんの一部を紹介しました。Scalaは学習コスト高めだと思いますが、慣れてくるとJavaよりもScalaの方がスッキリかけるし、便利メソッドがたくさん用意されているので扱いやすいと思います。