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

新人SEの学習記録

14年度入社SEの学習記録用に始めたブログです。気づけば社会人3年目に突入。

学習記録:Scala関数型デザイン

第4章:例外を使わないエラー処理(続き)

Optionデータ型

前節までで説明してきた問題の解決策は、問題への答えが常にあるとは限らないことを戻り値の型で表すことである。
つまり、エラー処理戦略を呼び出し元に委ねる、と考えることができる。

では実際に、Option型を作ってみる。

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

Optionには、定義可能なSomeになるケースと、未定義であるNoneになるケースが存在する。
このOptionをmeanの定義に使用すると、

def mean(xs: Seq[Double]): Option[Double] =
  if (xs.isEmpty) None
  else Some(xs.sum / xs.length)

結果が常に定義されるとは限らないことが戻り値の型に反映されている。
宣言された型の結果Option[Double]を常に返すため、meanは完全な関数になる。

Optionの使用パターン

関数型プログラミングでは、プログラミングの常連である部分関数にはOption(とEither)で対処するのが一般的である。
Scalaの標準ライブラリでは、Mapのキーによる検索ではOptionを返すなど、あちこちでOptionが使用されている。
以下では、Optionを操作するための基本的な関数をいくつか取り上げる。

なお、前章ではListの全関数をコンパニオンオブジェクトにまとめたが、
本章では可能な限り関数をOptionトレイトで定義し、func(obj, arg)ではなくobj.func(arg)で呼び出せるようにする。
これはあくまでもスタイル上の選択であり、本書では両方のスタイルを使用する。

sealed trait Option[+A]	{
  def map[B](f: A => B): Option[B]
  def flatMap[B](f: A => Option[B]): Option[B]
  def getOrElse[B>:A](default: => B): B
  def orElse[B>:A](ob: => Option[B]): Option[B]
  def filter(f: A => Boolean): Option[A]
}

Optionについては、要素を最大で一つ含むことのできるListのようなものと考えることができ、
Listの関数に類似する関数が存在する。
新しい構文として、default: => Bのアノテーション、B >: Aという型パラメータが挙げられる。
前者は非正格=関数で必要となるまで評価されず、後者はBがAと等しいかスーパークラスでなければならないことを示す。
こうする必要があるのは、AをOptionの共変パラメータとして宣言しても問題がないことをScalaに納得させるためである。

Exercise 4.1
  • 上記の関数(map, flatMap, getOrElse, orElse, filter)を全てOptionで実装せよ。

※Optionトレイト内の関数でトレイト外で宣言されているNoneなどを使用していることが原因で
 REPLでのload時でエラーになったが、以下のようにobjectで囲むことで解決。

object Hoge {
  sealed trait Option[+A] {
    def map[B](f: A => B): Option[B] = ...
  }
  case class Some[+A](get: A) extends Option[A]
  case object None extends Option[Nothing]
}

まずはmap。この関数は、OptionがNoneでない場合は関数fを適用する。

    // 値が入っていれば(Noneでなければ)関数fを適用                                          
    def map[B](f: A => B): Option[B] = this match {
      case None => None
      case Some(a) => Some(f(a))
    }
scala> val x: Option[Int] = Some(3)
x: Option[Int] = Some(3)

scala> val y: Option[Int] = None
y: Option[Int] = None

scala> x.map(_ * 2)
res1: Option[Int] = Some(6)

scala> y.map(_ * 2)
res2: Option[Int] = None

続いてgetOrElse。Some内の値を取り出す。Noneの場合は引数に入れたデフォルト値を得る。

    // 値が入っていれば値を、入っていなければdefaultを取得                                  
    def getOrElse[B>:A](default: => B): B = this match {
      case None => default
      case Some(a) => a
    }
scala> x.getOrElse(0)
res3: Int = 3

scala> y.getOrElse(0)
res4: Int = 0

flatMapはどういう関数なのかよくわからない。。。
mapでOptionの入れ子にして、それをgetOrElseで剥がす??

    def flatMap[B](f: A => Option[B]): Option[B] =
      map(f).getOrElse(None)

動作はこんな感じになるけど・・・?

scala> x.flatMap(a => Some(a * 2))
res10: Option[Int] = Some(6)

orElseはSomeならそのままSomeを、入ってなければ引数を取得。

    // 値が入っていればそのままSomeを、入っていなければobを取得                             
    def orElse[B>:A](ob: => Option[B]): Option[B] =
      map(Some(_)).getOrElse(ob)

getOrElseの戻り値はBなので、Option[B]を返そうと思うとthisがOption(Option[B])の状態で呼ばないといけない。

scala> x.orElse(Some(-1))
res17: Option[Int] = Some(3)

scala> y.orElse(Some(-1))
res18: Option[Int] = Some(-1)

最後にfilter。中身が関数fによる判定でtrueになればthisを返し、それ以外ならNoneになる。

    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)
scala> x.filter(_ > 1)
res20: Option[Int] = Some(3)

scala> x.filter(_ < 1)
res21: Option[Int] = None