読者です 読者をやめる 読者になる 読者になる

$shibayu36->blog;

株式会社はてなでエンジニアをしています。プログラミングや読書のことなどについて書いています。

ScalaのOptionとEitherで例外処理を行う方法


Scalaの例外処理はOptionとかEitherを利用するっぽいんだけど、調べてもいまいちその使い方が分からなかった。いろいろやってみたところ、だいぶ分かってきたので、後から自分で読み返せるようにメモ。

Optionを利用する

Optionは値があるかないかわからない場合に、ラップして返してくれるもの。値がある場合はSome()に値が包まれて返ってきて、ない場合はNoneが返ってくる。エラーの内容が特に必要がない場合の例外処理に便利。

パターンマッチで例外処理をする

SomeとNoneでパターンマッチすれば例外処理できる。こんな感じ。

val map = Map("a" -> 1, "b" -> 2)

map.get("a") match {
  case Some(n) => println(n)
  case None => println("Nothing")
}

map.get("c") match {
  case Some(n) => println(n)
  case None => println("Nothing")
}

MapのgetはOption型を返すので試せる。aというkeyは存在するので、最初のパターンマッチでは1がprintされる。cというkeyは存在しないので、二つ目のパターンマッチではNothingが表示される。

getOrElseを使う

単に入ってなかったらデフォルトを使いたいということであればgetOrElseを使ったらいい。

val map = Map("a" -> 1, "b" -> 2)

println(map.get("a").getOrElse(3))
println(map.get("c").getOrElse(3))

aというkeyは存在するので、一行目では1が返る。cというkeyは存在しないので二行目では3が返る。

flatMapとmapを利用する

flatMapとmapを利用することで、いくつかのOption型の値がすべてSomeだった場合に処理をするということもできる。

val map = Map("a" -> 1, "b" -> 2)

map.get("a").flatMap(a =>
  map.get("b").map(b => {
    println(a + b)
    a + b
  })
)
map.get("a").flatMap(a =>
  map.get("c").map(c => {
    println(a + c)
    a + c
  })
)
map.get("c").flatMap(c =>
  map.get("b").map(b => {
    println(b + c)
    b + c
  })
)

1つ目はaというkeyもbというkeyも存在するので、3と表示される。2つ目3つ目はcというkeyが存在しないので、表示が行われない。ちなみにflatMapとmapの組み合わせで返される型はOption型になっているので、1つ目はSome(3)、2つ目3つ目はNoneが返る。

flatMapとmapの代わりにforを利用する

flatMapとmapの組み合わせを利用しているときは、必ずforを利用できる。forの使い方によってはflatMapやmapの呼び出しに変換できるためである。

val map = Map("a" -> 1, "b" -> 2)

for {
  a <- map.get("a")
  b <- map.get("b")
} { println(a + b) }

for {
  a <- map.get("a")
  c <- map.get("c")
} { println(a + c) }

for {
  c <- map.get("c")
  b <- map.get("b")
} { println(c + b) }

これも一番上だけprintされて、他はprintされない。


また値を返したい場合はyieldを用いれば良い。

val map = Map("a" -> 1, "b" -> 2)

for {
  a <- map.get("a")
  b <- map.get("b")
} yield a + b

for {
  a <- map.get("a")
  c <- map.get("c")
} yield a + c

for {
  c <- map.get("c")
  b <- map.get("b")
} yield c + b

これは一番上だけSome(3)を返し、他はNoneを返す。


ちなみにforはforeachやfilterの呼び出しに変換する場合もある。このへんは今回は詳しくは述べない。

Eitherを利用する

どんなエラーかも含めて返したい場合はEitherを利用する。Eitherは成功したらRightに値を入れ、失敗したらLeftにエラーを入れて返すことで、呼び出し元で例外処理できる。

パターンマッチを利用する

基本はパターンマッチ。

簡単のため自分でEitherのオブジェクトを作ってやってみる。

val a: Either[String, String] = Right("a")
val b: Either[String, String] = Right("b")
val c: Either[String, String] = Left("c")

a match {
  case Right(str) => println("success: " + str)
  case Left(str)  => println("failed: " + str)
}

c match {
  case Right(str) => println("success: " + str)
  case Left(str)  => println("failed: " + str)
}

これはaの場合はRightに包まれているので、success: aと表示される。cの場合はLeftなのでfailed: cと表示される。これを利用すれば例外処理できる。

flatMapとmapを利用する

EitherもflatMapとmapを利用できるので、いくつかのEither型の値がすべてRightだった場合に処理をするということができる。ただしここで難しいのが、scalaのEitherはRight優先で使われるわけではなく、どちらに対して適用するか明示的に指定しなければならない(RightProjection型やLeftProjection型に変換しないといけない)。

val a: Either[String, String] = Right("a")
val b: Either[String, String] = Right("b")
val c: Either[String, String] = Left("c")

a.right.flatMap(a =>
  b.right.map(b => {
    println(a + b)
    a + b
  })
)

a.right.flatMap(a =>
  c.right.map(c => {
    println(a + c)
    a + c
  })
)

c.right.flatMap(c =>
  b.right.map(b => {
    println(b + c)
    b + c
  })
)

こういう感じで明示的にrightを呼び出して、それに対してflatMapやmapを呼ぶ。一番上はaもbも両方Rightなので"ab"と表示される。二つ目三つ目はcが無いために何も表示されず、返ってくるのはLeft("c")となる。

Eitherを使って嬉しいのはLeftがあったら、その後は無視してそのLeftを返してくれるところにある。例えば二つのLeftを使ったコードは以下のとおり。

val a: Either[String, String] = Left("a")
val b: Either[String, String] = Left("b")

a.right.flatMap(a =>
  b.right.map(b => a + b)
)

b.right.flatMap(b =>
  a.right.map(a => a + b)
)

この二つの結果は異なり、上はLeft("a")が、下はLeft("b")が返ってくる。Leftがあったら途中で計算を諦めて、そのLeftを返してくれるのでどこで例外が起こったかも知ることができる。

forを利用する

Optionでも紹介したとおり、flatMapとmapの組み合わせはforに置き換えられる。

val a: Either[String, String] = Right("a")
val b: Either[String, String] = Right("b")
val c: Either[String, String] = Left("c")

for {
  val_a <- a.right
  val_b <- b.right
} yield val_a + val_b

for {
  val_c <- c.right
  val_b <- b.right
} yield val_c + val_b

for {
  val_a <- a.right
  val_c <- c.right
} yield val_a + val_c

これは一つ目は両方共RightなのでRight("ab")が返る。二つ目はcがLeft("c")なのでそこで計算を終了し、Left("c")を返す。

Option.toRight

ちなみにOption型からRightやLeftに変換することもできる。これを利用するとOptionで取ってきたものがなかった場合に、そのエラーを返すこともできる。toRightはSomeだったら中身をRightにくるんで返し、Noneだったら指定した値をLeftにくるんで返す。

val map = Map("a" -> 1, "b" -> 2)

for {
  val_a <- map.get("a").toRight("a: Not Found").right
  val_b <- map.get("b").toRight("b: Not Found").right
} yield val_a + val_b

for {
  val_a <- map.get("a").toRight("a: Not Found").right
  val_c <- map.get("c").toRight("c: Not Found").right
} yield val_a + val_c

これは一つ目の式はaとb両方のkeyが存在するので、Right(3)が返る。しかし二つ目の式はcが存在しないため、Left("c: Not Found")が返る。

まとめ

今回はOptionやEitherを使った例外処理の方法について簡単にまとめた。ようやく使い方はなんとなくわかったような気がする。最初の頃はScalaやってる人がEitherが右寄りじゃなくて困ると言ってたのがよく分からないけど、こうやってまとめてみると毎回.rightを呼び出さないといけなかったりして不便。

またこの辺りがわかっていると、以下の記事を読むことでさらに知識が深められるのでオススメです。


yuroyoro.hatenablog.com
yuroyoro.hatenablog.com
hakobe932.hatenablog.com