SE情報技術研究会’s blog

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

2015-12-05 第9回 関数型プログラミング勉強会(Scala)の振り返り

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

前回の続きより

Either

一般的に左(Left)がエラー、右(Right)が正(right=正しいという意味)の結果。

注釈に

※11 Eitherは、新しいデータ型を定義するほどではないケースにおいて、 2つの可能性のうちの1つを符号化する目的でもよく使用される。 どちらかと言えば、こちらのほうが一般的である。

とあるが、意味がよくわからない。 ペアとして使うわけではないだろうし…という意見も出たが、結局はよくわからないということに… Eitherはエラーか正常な結果を返す以外に使うこともあるものなのか?

本書には、その例がいくつか含まれている。

ということなので後で具体的な例が出てくるのかもしれない。

Exercise 4.6

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

Exercise 4.7

GitHubにあがってるもので特に問題はないが、foldRightでも実装した参加者もいた。

GitHubの答えのtraverse関数を見てみる。

def traverse[E,A,B](es: List[A])(f: A => Either[E, B]): Either[E, List[B]] = 
  es match {
    case Nil => Right(Nil)
    case h::t => (f(h) map2 traverse(t)(f))(_ :: _)
  }

ここで使われているmap2の定義は下記。

def map2[EE >: E, B, C](b: Either[EE, B])(f: (A, B) => C): 
  Either[EE, C] = for { 
      a <- this;
      b1 <- b
  } yield f(a,b1)

試しに例外が発生しない場合を段階的に見ていくと

traverse(List(1,2,3))(a => Try(a *2)) =
↓ f = a => Try(a *2)とする
traverse(Cons(1,Cons(2,Cons(3, Nil)))(f)
↓
f(1) map2 traverse(Cons(2,Cons(3, Nil)))(f)(_::_)
↓
f(1) map2 (f(2) map2 traverse(Cons(3, Nil))(f)(_::_))(_::_)
↓
f(1) map2 (f(2) map2 (f(3) map2 traverse(Nil)(f)(_::_))(_::_))(_::_)
↓
f(1) map2 (f(2) map2 (f(3) map2 Right(Nil)(_::_))(_::_))(_::_)
↓
f(1) map2 (f(2) map2 (Right(6) map2 Right(Nil)(_::_))(_::_))(_::_)
↓
f(1) map2 (f(2) map2 (Right(Cons(6, Nil)))(_::_))(_::_)
↓
f(1) map2 (Right(Cons(4, Cons(6, Nil))))(_::_)
↓
Right(Cons(2, Cons(4, Cons(6, Nil))))
↓
Right(List(2,4,6))

となる。

例外が発生する場合を段階的にみると

traverse(List(0,1,2))(a => Try(10/a)) =
↓ f = a => Try(10/a)
traverse(Cons(0,Cons(1,Cons(2, Nil)))(f)
↓
f(0) map2 traverse(Cons(1,Cons(2, Nil)))(f)(_::_)
↓
Left(Exception) map2 traverse(Cons(1,Cons(2, Nil)))(f)(_::_)
↓
Left(Exception) map2 Right(List(2,4))(_::_)
↓
Left(Exception)

となる。

Exercise 4.8

解答には説明だけあったので見ていく。

There are a number of variations on Option and Either. If we want to accumulate multiple errors, a simple approach is a new data type that lets us keep a list of errors in the data constructor that represents failures:

OptionEitherにはたくさんの種類がある。 もし複数のエラーをまとめたいのなら、単純な方法は 失敗を表すデータコンストラクタにエラーのリストを持つことができる 新しいデータ型をつくることである。

trait Partial[+A,+B]
case class Errors[+A](get: Seq[A]) extends Partial[A,Nothing]
case class Success[+B](get: B) extends Partial[Nothing,B]

There is a type very similar to this called Validation in the Scalaz library. You can implement map, map2, sequence, and so on for this type in such a way that errors are accumulated when possible (flatMap is unable to accumulate errors--can you see why?).

Scalazライブラリにほぼ同じValidationという型がある。 map, map2, sequence, などをエラーを可能な限りまとるこの型で実装できる。 (flatMapはエラーをまとめれない。--なぜでしょう?)

This idea can even be generalized further-- we don't need to accumulate failing values into a list; we can accumulate values using any user-supplied binary function.

この考えはさらに一般化できる。 リストに失敗した値をまとめる必要はない。 ユーザが提供した二項関数を使って値をまとめることも可能だ。

It's also possible to use Either[List[E],_] directly to accumulate errors, using different implementations of helper functions like map2 and sequence.

map2 and sequenceなど補助的な関数を別に実装したものを使って、 Either[List[E],_]で直接エラーをまとめることも可能だ。

Partialというのは「部分的」というような意味だと思うので、 ErrorsやSuccessなど例外処理のクラス定義をするのは抵抗があるが、どうなのだろう?

他の言語にはPartialというクラスが存在するらしい。 が、C#の場合、エラー処理としては使われてないもよう。

複数のエラーをまとめたい場合でEither[List[E],_]の形式をとる場合、 現状のTry関数はエラーが発生した際にそのエラーをそのまま返しているのを新たに修正すれば、 確かにmap2など関数を駆使すればEither[List[E],_]を返す関数にすることも可能とは思える。

List[Left] から Left[List]にするような関数をつかって行うイメージ。