Scala関数型デザイン&プログラミング 〜4章 (4)
"Scala関数型デザイン&プログラミング ―Scalazコントリビューターによる関数型徹底ガイド"を買いました。
ちょっとずつ読んで、各章の内容をまとめてゆきます。
今回は4章まで。実用的なエラー処理についてです。
4章
この章では、エラー処理について触れています。
まず、例外が参照透過でないこと、型安全でないこと、mapなどの汎用性の高い関数との相性が最悪であることが述べられています。
代わりにnullや特殊な値を返す方法、呼び出し元でエラー処理関数を引数として渡す方法などを挙げて問題点を列挙しています。
Option
これらに代わるアプローチとして、Option型が紹介されます。
ここで早速練習問題が出ます。
Option型に対するmap, flatMap, getOrElse, orElse, filterを実装せよ。
def map[B](f: A => B): Option[B] =
this match {
case None => None
case Some(a) => Some(f(a))
}
def getOrElse[B>:A](default: => B): B =
this match {
case None => default
case Some(a) => a
}
def flatMap[B](f: A => Option[B]): Option[B] =
this.map(f).getOrElse(None)
def orElse[B>:A](ob: => Option[B]): Option[B] =
this.map(Some(_)).getOrElse(ob)
def filter(f: A => Boolean): Option[A] =
this.flatMap(a => if(f(a)) Some(a) else None)
Optionがネストしたりして少しややこしいですが、必要な処理と必要な型を考えるとそこそこスムーズに書くことが出来ました。
flatMapをベースとして、variance関数を実装せよ。
エラー処理を気にせず書くと出来上がりです。
def variance(xs: Seq[Double]): Option[Double] =
mean(xs).flatMap(m => mean(xs.map(a => math.pow(a - m, 2))))
便利で安全なOptionですが、今までの関数を全て対応するよう書き換えないといけない?という不安があがってきます。
そんな時のためにliftという考えを使って、普通の関数をOption対応させることが可能です。
2項関数を使ってOption型を2つ受け取るmap2を作れ。
def map2[A,B,C](a: Option[A], b: Option[B])(f: (A, B) => C): Option[C] =
a.flatMap(sa => b.map(sb => f(sa, sb)))
配列に対しNoneを1つでも含む場合はNoneを返し、それ以外は全てのSomeを返すsequence関数を作れ。
foldRightでリストを作る途中で、先ほどのmap2を使えば良さそうです。
def sequence[A](a: List[Option[A]]): Option[List[A]] =
a.foldRight(Some(Nil): Option[List[A]]) ((x, y) => map2(x, y)(_ :: _))
1度しか走査を行わない、sequenceを総称したtraverse関数を作れ。
上記sequenceととほとんど同じですね。
def traverse[A, B](a: List[A])(f: A => Option[B]): Option[List[B]] =
a.foldRight(Some(Nil): Option[List[B]]) ((x, y) => map2(f(x), y)(_ :: _))
Either
Optionはエラーが起きたことを知らせてくれますが、追加情報を与えてはくれません。
そこで、Eitherデータ型を使って情報を付与したエラーを表現します。
Eitherは、Right(正常値)とLeft(エラー値)の直和からなるデータ型です。
どちらかに値が入るわけですね。
またまた早速練習問題です。
Right値を操作するmap/flatMap/orElse/map2を作れ。
def map[B](f: A => B): Either[E, B] =
this match {
case Right(v) => Right(f(v))
case Left(v) => Left(v)
}
def flatMap[EE >: E, B](f: A => Either[EE, B]): Either[EE, B] =
this match {
case Right(v) => f(v)
case Left(v) => Left(v)
}
def orElse[EE >: E, B >: A](b: => Either[EE, B]): Either[EE, B] =
this match {
case Right(v) => Right(v)
case Left(_) => b
}
def map2[EE >: E, B, C](b: Either[EE, B])(f: (A, B) => C): Either[EE, C] =
flatMap(a => b.map(f(a, _)))
flatMapとmapの組み合わせは、syntax sugarが用意されていてforで書けます。
まだ定着していないのでflatMap/mapで書いてしまいましたが、forにも慣れていきたいです。
sequence, traverseを作れ。
def sequence[E,A](es: List[Either[E,A]]): Either[E,List[A]] =
es.foldRight(Right(Nil): Either[E, List[A]])((a, b) => a.map2(b)(_ :: _))
def traverse[E,A,B](es: List[A])(f: A => Either[E, B]): Either[E, List[B]] =
es.foldRight(Right(Nil): Either[E, List[B]])((a, b) => f(a).map2(b)(_ :: _))
ほとんど同じなので、sequenceはtraverseを使って書くこともできますね。
traverseのAがEither[E, B]になればよいです。
map2でつなげた際、複数のエラーが起きていても1つしか報告できない。何を変更すればよいか。
Eitherは1つしかエラーを持てないので、Leftをリストにしてあげれば複数持てそうです。
エラーが発生するたびにリストに追加してゆくイメージです。
ここで4章は終了です。
3章の怒涛の練習問題の後だと、多少物足りない感はあります。
が、まだまだ先は長いですので、各章はこれくらいの長さが良かったりします。
次章では非正格の話です。
徐々に深いところに潜っている感じがします。
思ったこと
本章ではエラー処理を扱いましたが、実際のプログラムでも特に役立つ章だと思いました。
例外を使えば一瞬で片付きますが、フローが乱れたり型安全が崩れたり処理し忘れたりと、デメリットも大きいです。
エラーも値として後でまとめて処理する、純粋性を保つための工夫が詰め込まれていると感じました。
続き: