SE情報技術研究会’s blog

http://se-info-tech.connpass.com

2015-11-21 第8回 関数型プログラミング勉強会の簡単なまとめ

第4章 例外を使わないエラー処理

例外を使わないほうが良い理由

  • 参照透過性が失われる。
  • 例外で制御フローを行うと例外ベースの複雑なコードが必要になる
  • Scalaの場合、例外にIntなどの任意の型を割り当てれるため型安全ではない
  • 高階関数で検査例外を扱うと、関数の呼び出し元が実行される様々な例外に対して対処せねばならなくなる

例外はエラー処理のみに使うべき

p.69:

慣例では、OptionのNoneケースを例外に戻すためにo.getOrElse(throw new Exception(“FAIL”))を実行します。 原則として、例外を使用するのは、例外をキャッチするのにふさわしいプログラムが存在しない場合だけにしてください

例外を使わない場合の対応

  • 対象の型の偽の値(例:Double.NaNなど)や null を返す。→問題が多いので却下
  • 問題となる場合の戻り値を引数に追加。 →呼び出し元が例外となるケースの処理を知っておく必要があるので却下。
  • Optionの使用。

Exercise 4.1

GitHubに挙げられている内容で問題なし

Exercise 4.2

GitHubの解答によると別メソッドでmeanも定義されていて、それも定義している。

def variance(xs: Seq[Double]): Option[Double] = 
  mean(xs) flatMap (m => mean(xs.map(x => math.pow(x - m, 2))))

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

試しにList(1,2,3)で実行してみる

variance(List(1,2,3)): Option[Double] = 
↓
mean(List(1,2,3)) flatMap (m=>mean(List(1,2,3).map(x => math.pow(x-m,2))))
↓
Some(2) flatMap (m=>mean(List(1,2,3).map(x => math.pow(x-m,2))))
↓ m = 2
mean(List(math.pow(1-2,2), math.pow(2-2,2), math.pow(3-2,2)))
↓
mean( List((-1)^2, 0^2, 1^2))
↓
mean( List(1, 0, 1))
↓
Some(2/3)
↓
0.666666...

途中で0割ってどうなのかが、気になり試してみる

val val1 = 0.0 / 0.0
val val2 = 0.1 / 0.0

この場合、val1=Nan、val2=Infinityとなり0割の例外は発生しなかった。 どうもdoubleだと、こういう仕様らしい。

ちなみにintの0割は実行時に例外が発生する。

Optionを使うように変更することによる既存のプログラムへの影響は基本ない

既存の関数はそのままにOptionに対応する関数liftで対応可能(p.70)

    def lift[A, B](f: A => B): Option[A] => Option[B] = _ map f

圏論の世界だと、ある関数を別の型にも適用させるようなことはよくある手段らしい。

Exercise 4.3

GitHubの解答だと下記になる。

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)))

a と b のそれぞれがNoneのケースを試してみる

  • a が None の場合、flatMap関数は None を返す
  • b が None の場合、map関数が None を返し、そのまま戻り値も None となる

ただし、下記のようにNoneをcaseで分けたほうが何をしたいのか、わかりやすいと思われる

  def map2_2[A, B, C](optA: Option[A], optB: Option[B])(f: (A, B) => C): Option[C] =
    (optA, optB) match {
      case (None, _) => None
      case (_, None) => None
      case (Some(a), Some(b)) => Some( f(a, b) )
    }

Exercise 4.4

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 :: _))
  }

ここで、Listの中にNoneが含まれていた場合、つまり再帰のどこかでh=Noneの場合、flatMap は None を返す。 よって戻り値もNoneとなる。

Some(Nil)はOptionの値がない場合ではなくListの最後の要素。これがNoneを返すと結果がNoneになるのでNG。 ただ、ListのaがNilの場合、Some(Nil)が帰るのでなんとなくしっくりこない。

traverse関数

仕様はList[A]に対し関数f:A=>Option[B]を渡すことでOption[List[B]]を作成する。

traverseは「旅する」というイメージがある。検索などでよく使われるメソッド名。

Exercise 4.5

GitHubにあがってるもので特に問題はない