[初級-中級向け]Scala基本APIを完全に理解するシリーズ② -Either編-
はじめに
[初級-中級向け]Scala基本API紹介シリーズ① -Option編-の続きです。
EitherはScalaの中心的な役割を担うクラスです。
Optionなどの他のクラスと違って少し癖があるため最初は戸惑うかも知れません。
しかしマスターすれば強力なバグ抑制機構になってくれます。
では行きましょう!
使用頻度・重要度
メソッドが多すぎするとどれが重要なのか分かりづらいので各メソッドの横にランク付けをしておきます。
☆☆☆: 非常によく使う。Scalaを書くなら必須レベル
☆☆: 使いどこでは威力を発揮する。これを使いこなすかどうかで、コードの綺麗さが変わる。
☆: あまり使わない。使いたいときにどうぞ。場合によっては使用しないほうがいいことも。
Either/Right/Left objectメソッド
apply ☆☆☆
RightとLeftに生えてるコンストラクタ。
普通に生成されます。
scala> Right(1)
res0: scala.util.Right[Nothing,Int] = Right(1)
scala> Left(1)
res1: scala.util.Left[Int,Nothing] = Left(1)
cond ☆☆☆
隠れていますが、こいつを使いこなすかどうかで結構コード量が変わってきます。
Either生成のよくあるケースとしては条件によってRightかLeftかを振り分けるというものだと思います。
このメソッドに条件、Rightのときの値、Leftのときの値を渡せば勝手にやってくれます。
// こういうやつが
scala> if (true) Right("ok") else Left("fail")
res0: scala.util.Either[String,String] = Right(ok)
// こんな感じで書ける。
scala> Either.cond(true, "ok", "fail")
res1: scala.util.Either[String,String] = Right(ok)
scala> Either.cond(false, "ok", "fail")
res2: scala.util.Either[String,String] = Left(fail)
Either クラスメソッド
map ☆☆☆
Rightの場合のみに中身を変換したいときによく使います。
挙動としてはOptionのSomeに似ています。
for式でも書くことができます。
Option同様非常によく使用するので必ずマスターしましょう。
scala> Right(1).map(_ * 2)
res0: scala.util.Either[Nothing,Int] = Right(2)
scala> val l: Either[Int, Int] = Left(1)
l: Either[Int,Int] = Left(1)
scala> l.map(_ * 2)
res1: scala.util.Either[Int,Int] = Left(1)
// for式で同じことが書ける
scala> for {
| r <- Right(1)
| } yield r * 2
res2: scala.util.Either[Nothing,Int] = Right(2)
flatMap ☆☆☆
Eitherにはflattenはありませんが、flatMapはOption同様Eitherになる処理を行った後に一つEitherを剥がしてくれます。
直接的に使用することもあれば、for式のジェネレータ構文で間接的に使用することもあり、非常によく使うメソッドです。
個人的にはfor式のほうがスッキリして好みです。
ScalaはmapとflatMapでできています。
// 2つの式は同じものを返す
scala> Right(1).flatMap(r => Right(2).map(rr => r + rr))
res0: scala.util.Either[Nothing,Int] = Right(3)
scala> for {
| r <- Right(1) // ここの<-はflatMap
| rr <- Right(2) // ここの<-はmap
| } yield r + rr
res1: scala.util.Either[Nothing,Int] = Right(3)
foreach ☆
Rightのときのみなにか処理を行いときに使用します。
Optionの時同様Unitが返るので、関数型を好むScalaではあまり使わないようにしましょう。
scala> Right(1).foreach(println)
1
scala> Left(1).foreach(println)
// 何も表示されない
isRight/isLeft ☆☆
RightかLeftかを判定します。
基本的にはmapを使用して、それができないときにこれらのメソッドを使用しましょう。
scala> val r = Right(1)
r: scala.util.Right[Nothing,Int] = Right(1)
// こういう返り値が多様なものはmapじゃ厳しい
scala> if (r.isRight) r else println("left")
res0: Any = Right(1)
getOrElse ☆☆
Rightのときに中身を取り出し、Leftのときにデフォルト値を取得します。
Optionと同じでEitherであること自体がそもそも意味を持つ(処理が成功したのか失敗したのかとか)ので、無闇に使用しないようにしましょう。
一連の処理の最後で呼び
出すなどが望ましい使い方です。
scala> val r: Either[Int, Int] = Right(1)
r: Either[Int,Int] = Right(1)
// こういうやつが
scala> r match {
| case Right(v) => v
| case Left(_) => 2
| }
res0: Int = 1
// こう書ける
scala> Right(1).getOrElse(2)
res1: Int = 1
scala> Left(1).getOrElse(2)
res2: Int = 2
filterOrElse ☆☆
Rightでフィルターを通る、もしくはLeftのときにそのまま値を返します。
逆にRightでフィルターを通らないときにはデフォルトで設定した値がLeftで返ります。
RightをフィルターしてLeftにしていきたいときなどに使用できます。
連続で適用すればすべてのフィルターを生き残ったRightを取得、みたいな使い方もできます。
scala> Right(12).filterOrElse(_ > 10, -1)
res0: scala.util.Either[Int,Int] = Right(12)
scala> Right(7).filterOrElse(_ > 10, -1)
res1: scala.util.Either[Int,Int] = Left(-1)
scala> Left(7).filterOrElse(_ => false, -1)
res2: scala.util.Either[Int,Nothing] = Left(7)
scala> Right("ab").filterOrElse(_.endsWith("b"), "not end").filterOrElse(_.startsWith("a"), "not start")
res3: scala.util.Either[String,String] = Right(ab)
swap ☆☆
RightとLeftを入れ替えます。
Rightな値とLeftな値同士を用いた処理などを書くときに利用できます。
scala> val right = Right(2)
right: scala.util.Right[Nothing,Int] = Right(2)
scala> val left = Left(3)
left: scala.util.Left[Int,Nothing] = Left(3)
scala> for {
| r1 <- right
| r2 <- left.swap // ここでLeftをRightに変換してあげないと中の値を取り出せない
| } yield r1 * r2
res0: scala.util.Either[Nothing,Int] = Right(6)
contains ☆☆
Rightかつ引数の値と等しいかのチェックを行います。
Leftの場合には即falseです。
つまり、Rightかつ値と等しい場合のみtrueを返します。
// こういうのが
scala> e match {
| case Right(v) => v == 1
| case Left(_) => false
| }
res0: Boolean = true
// こう書ける
scala> e.contains(1)
res1: Boolean = true
exists ☆☆
Rightのときに検査を行い結果をBooleanで返却します。
Leftのときは問答無用でfalseを返します。
Containsよりも柔軟にRightの中身を検査できます。
ただし同値比較のときはcontainsのほうが意味が明確になるため、そちらの方を使用したほうがいいでしょう。
scala> Right(1).contains(1)
res0: Boolean = true
scala> Right(1).exists(_ == 1)
res1: Boolean = true
scala> Right(1).exists(_ % 2 == 1)
res2: Boolean = true
forall ☆☆
Rightのときに検査を行うのはexistsと同様ですが、Leftのときにはtrueが返ります。
検査対象が無いときにはそもそもテストが通っても通らなくても真というのがforallの意味です。
トリッキーな動きをするので使用する機会は少ないですが、使用できる場面で使うとかっこいいです。
scala> val e: Either[Int, Int] = Right(1)
e: Either[Int,Int] = Right(1)
scala> val p = (a: Int) => a % 2 == 0
p: Int => Boolean = $$Lambda$6157/1332242319@7351b15a
// こういうのが
scala> e match {
| case Right(v) => p(v)
| case _ => true
| }
res0: Boolean = false
// こう書ける
scala> e forall p
res1: Boolean = false
fold ☆☆☆
RightかLeftによって処理を振り分けることができます。
特に関数型でまともに書くと、一連の処理の最後に結局Right/Leftどっちなのかによってmatch caseを書くことはザラにあるためfoldを使用することをおすすめします。
scala> val e: Either[Int, Int] = Right(1)
e: Either[Int,Int] = Right(1)
scala> val fa = (_: Int) => print("right")
fa: Int => Unit = $$Lambda$6232/529040566@7cafa6eb
scala> val fb = (_: Int) => print("left")
fb: Int => Unit = $$Lambda$6233/209463406@2e6905f9
// こういう処理が
scala> e match {
| case Right(v) => fa(v)
| case Left(v) => fb(v)
| }
right
// こうやって書ける
scala> e.fold(fb, fa)
right
toOption/toSeq/toTry ☆☆☆
RightをSome/要素が一つのSeq/Successへmappingし、LeftをNone/空のSeq/Failureへと変換します。
Scalaはこれらのクラスを処理の流れの中で使用した
結果、各クラスを一つにまとめ上げるという処理が頻発します。
そのときにこれらのメソッドで変換することでシンプルに変換処理を記述することができます。
scala> val e: Either[Int, Int] = Right(1)
e: Either[Int,Int] = Right(1)
// こういうのが
scala> Some(e) flatMap {
| case Left(_) => None
| case Right(v) => Some(v)
| }
res0: Option[Int] = Some(1)
// こうかけてシンプル
scala> Some(e).flatMap(_.toOption)
res1: Option[Int] = Some(1)
// forを使うことも可能
scala> for {
| ee <- Some(e)
| v <- ee.toOption
| } yield v
res2: Option[Int] = Some(1)
joinLeft/joinRight ☆☆
Eitherがネストした場合にネストを一つ剥がしてくれます。
Eitherは右と左という対等な概念があるせいで、単純にflattenができません。
そのためこのjoin系のメソッドで型に応じて適切にネストを消してくれます。
joinLeftはLeftがネストした場合にのみLeftを返し、joinRightはRightがネストした場合にのみRightを返します。
joinLeft
- LeftとRightがネストしたときはRight
scala> Left[Either[Int, String], String](Right("flower")).joinLeft
res0: scala.util.Either[Int,String] = Right(flower)
// Leftがネストした場合のときのみLeft
scala> Left[Either[Int, String], String](Left(12)).joinLeft
res1: scala.util.Either[Int,String] = Left(12)
// そもそもRightのときにはRight
scala> Right[Either[Int, String], String]("daisy").joinLeft
res2: scala.util.Either[Int,String] = Right(daisy)
joinRight
- RightがネストしたときのみRight
scala> Right[String, Either[String, Int]](Right(12)).joinRight
res3: scala.util.Either[String,Int] = Right(12)
// RightとLeftがネストしたり、
scala> Right[String, Either[String, Int]](Left("flower")).joinRight
res4: scala.util.Either[String,Int] = Left(flower)
// そもそもLeftのときにはLeft
scala> Left[String, Either[String, Int]]("flower").joinRight
res5: scala.util.Either[String,Int] = Left(flower)
left/rightメソッドとProjectionクラス ☆☆☆
ここまで読まれた方でEitherの使用方法に疑問を感じた方もいるかもしれません。
なぜならEitherにおいてLeft/Rightは対等なはずなのにRightがあまりにも優遇され過ぎだからです。
mapを始めとし、containsやforAllまでRightを中心としてAPIが組み立てられています。
理由は単純です。
Optionなどと違い、EitherはLeftの中身・Rightの中身という2つの対等な値がある以上、どちらかを選択してmapなどの処理を組み建てる必要があるからです。
Scalaでは慣習的にRightを正常系として扱うため、Rightが優先されているのでしょう。
ではLeftを中心として処理を組み立てたい時、Rightの様に優秀なAPIを利用して処理を組み立てることはできないのでしょうか?
もちろんあります。
それがleft/rightメソッドを介して生成されるLeftProjection/RightProjectionクラスです。
これらは明示的にどちら側の値にmapなどの処理を適用するかを定めたクラスです。
RightProjectionクラスは普通のEitherクラスと挙動が変わりませんが、LeftProjectionクラスはLeftを中心としてAPIが組み直されています。
そのためLeftな値に対して
- map
- get
- exists
- filter
- flatMap
- foreach
- forall
- getOrElse
- toOption/toSeq
メソッドを使用したいときにはEitherをleftにprojectionして使用するようにしましょう。
終わりに
EitherはOption同様Scalaの中心を担うライブラリです。
必ずマスターするようにしましょう。
君も今日からScalaマスター!