ScalaのOption(Scala関数型デザインより)

Scala関数型デザイン&プログラミング」の「第 4 章   例外 を 使わ ない エラー 処理」をやる。

package example

sealed trait Option[+A] {

  def map[B](f: A => B): Option[B] = this match {
    case None => None
    case Some(x) => Some(f(x))
  }

  def flatMap[B](f: A => Option[B]): Option[B] = this match {
    case None => None
    case Some(x) => f(x)
  }

  def flatMap2[B](f: A => Option[B]): Option[B] =
    this map(f) getOrElse None

  def getOrElse[B>:A](default: => B): B = this match {
    case None => default
    case Some(a) => a
  }

  def orElse[B>:A](ob: => Option[B]): Option[B] = this match {
    case None => ob
    case _ => this
  }

  def orElse2[B>:A](ob: => Option[B]): Option[B] =
    this map(Some(_)) getOrElse ob

  def filter(f: A => Boolean): Option[A] = this match {
    case Some(a) if (f(a)) => this
    case _ => None
  }

  def filter_1(f: A => Boolean): Option[A] =
    flatMap(a => if (f(a)) Some(a) else None)

  // Option対応されていない関数(f: A => B)をOptionでくるまれた引数を取れるようにする
  def lift[A, B](f: A => B): Option[A] => Option[B] = _ map f

  def map2[A,B,C](a: Option[A], b: Option[B])(f: (A, B) => C): Option[C] =
    a flatMap(aa => b map (bb => f(aa, bb)))

  def sequence[A](a: List[Option[A]]): Option[List[A]] = a match {
    case Nil => Some(Nil)
    //case h :: t => h flatMap (hh => sequence(t) map (hh :: _))
    case Cons(h,t) => h flatMap (hh => sequence(t) map (Cons(hh, _)))
    //(h flatMap)は、OptionのflatMapを読んでいる
  }

  def traverse[A, B](a: List[A])(f: A => Option[B]): Option[List[B]] = a match {
    case Nil => Some(Nil)
    case Cons(h,t) => map2(f(h), traverse(t)(f))(Cons(_,_))
  }

  def sequenceViaTraverse[A](a: List[Option[A]]): Option[List[A]] =
    traverse(a)(x => x)
}

case class Some[+A](get: A) extends Option[A]
case object None extends Option[Nothing]

object Option {

  def calcSlayer(n: Int): Option[Int] =
    if (n > 5) Some(n) else None

  def main(args: Array[String]): Unit = {
    val n = calcSlayer(99)
    println(s"n:${n}")
    println(s"n:${n.getOrElse(0)}")
    println(s"none:${None.getOrElse(0)}")

    //val m = n flatMap(aa => Some(aa))
    val m = n.flatMap(calcSlayer)
    println(s"m:${m}")
    println(s"m:${m.getOrElse(0)}")

    val fnZ = n.lift((a: Int) => a + 100)
    println(s"fnZ:${fnZ(Some(7))}")

    val absO = misia.lift(math.abs)
    println(s"absO:${absO(Some(-11))}")
    val absb = (Some(-11)).map(math.abs)
    println(s"absb:${absb}")

    val misia = None
    val s1 = Some(1)
    val s2 = Some(2)
    val result = misia.map2(s1,s2)((a,b) => a + b)
    println(s"result:${result}")

    val lstSome = List(s1,s2)
    val optLst = misia.sequence(lstSome)
    println(s"lstSome:${lstSome}")
    println(s"optLst:${optLst}")

    val lstNone = List(s1,None,s2)
    val optNoneLst = misia.sequence(lstNone)
    println(s"lstNone:${lstNone}")
    println(s"optNoneLst:${optNoneLst}")

  }

}

Optionのメソッドは、traitに登録する。 なので、Optionのメソッド達を使うためには、インスタンスが必要。

    val misia = None
    val s1 = Some(1)
    val s2 = Some(2)
    val result = misia.map2(s1,s2)((a,b) => a + b)
    println(s"result:${result}")

mapとflatMap

  def map[B](f: A => B): Option[B] = this match {
    case None => None
    case Some(x) => Some(f(x))
  }

  def flatMap[B](f: A => Option[B]): Option[B] = this match {
    case None => None
    case Some(x) => f(x)
  }

  def flatMap2[B](f: A => Option[B]): Option[B] =
    this map(f) getOrElse None
  • mapとflatMapは普通にパターンマッチで、NoneとSomeを見てやればよい。
  • flatMapの場合は、fがOptionでくるんで返すので、Someの場合、そのままfを呼ぶだけで良い。

getOrElse

  def getOrElse[B>:A](default: => B): B = this match {
    case None => default
    case Some(a) => a
  }

  def orElse[B>:A](ob: => Option[B]): Option[B] = this match {
    case None => ob
    case _ => this
  }

  def orElse2[B>:A](ob: => Option[B]): Option[B] =
    this map(Some(_)) getOrElse ob

こちらもパターンマッチでやる。 以下のような形で使う。

    println(s"${None.getOrElse(0)}")

filter

  def filter(f: A => Boolean): Option[A] = this match {
    case Some(a) if (f(a)) => this
    case _ => None
  }

  def filter_1(f: A => Boolean): Option[A] =
    flatMap(a => if (f(a)) Some(a) else None)
  • こちらもパターンマッチングで。caseの条件にifを入れるのがミソ。

lift

  // Option対応されていない関数(f: A => B)をOptionでくるまれた引数を取れるようにする
  def lift[A, B](f: A => B): Option[A] => Option[B] = _ map f

lift: Option対応されていない関数(f: A => B)をOptionでくるまれた引数を取れるようにする 「通常の関数をリフトして、Option対応にする。」という

    val absO = misia.lift(math.abs)
    println(s"absO:${absO(Some(-11))}")
    val absb = (Some(-11)).map(math.abs)
    println(s"absb:${absb}")

絶対値返す関数(math.abs)をOption対応すると、 実行には、Optionを渡してあげる
mapをそのまま使う場合には、Option値に対して、直接実行する形となる

  def lift[A, B](f: A => B): Option[A] => Option[B] = _ map f
liftの式の(_ map f)の「_」を「this」に変えると、その場で実行してしまう形となる。mapを実行した結果のOption[B]の値を返してしまうため、型があわずコンパイルエラーとなる。
「_」であれば、値を返すのではなく、fを「Option[A] => Option[B]」に変換した関数を返す。

map2

  def map2[A,B,C](a: Option[A], b: Option[B])(f: (A, B) => C): Option[C] =
    a flatMap(aa => b map (bb => f(aa, bb)))

2項関数をOption対応にする。 flatMapとmapの内と外を間違えやすいが、flatMapに渡す関すは、(f: A => Option[B])だから、mapが内。
flatMapとmapで、つつまれたものを、ほどいた後、fに適用する。

sequenceとtraverse

  def sequence[A](a: List[Option[A]]): Option[List[A]] = a match {
    case Nil => Some(Nil)
    //case h :: t => h flatMap (hh => sequence(t) map (hh :: _))
    case Cons(h,t) => h flatMap (hh => sequence(t) map (Cons(hh, _)))
    //(h flatMap)は、OptionのflatMapを読んでいる
  }

  def traverse[A, B](a: List[A])(f: A => Option[B]): Option[List[B]] = a match {
    case Nil => Some(Nil)
    case Cons(h,t) => map2(f(h), traverse(t)(f))(Cons(_,_))
  }

  def sequenceViaTraverse[A](a: List[Option[A]]): Option[List[A]] =
    traverse(a)(x => x)

sequenceは、Optionでくるまれた、リストを入れ替えて、リストをOptionにくるんで返す。
traverseは、リストにする際に関数を適用して、Optionにくるんで返す。
なので、sequenceは、何もしない関数をtraverseに渡せばよい。

    case Cons(h,t) => map2(f(h), traverse(t)(f))(Cons(_,_))

traverseの、「map2(f(h), traverse(t)(f))(Cons(,))」は、map2でほどいて、Consするが、リストのtailをtraverse(t)(f)で、再帰して取り出している。