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

新人SEの学習記録

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

学習記録:Scala関数型デザイン 第4章、購入した本

関数型 プログラミング 学習記録 Scala

参考文献

Scala関数型デザイン&プログラミング―Scalazコントリビューターによる関数型徹底ガイド

Eitherデータ型

概要

エラー処理にOptionはよく用いられるが,例外的な状況で何がうまく行かなかったのかは教えてくれない。
さらに状況を提供するためのStringだったり,何のエラーだったのかの情報であったりが必要なときもあるだろう。
ここでは,Optionの単純な拡張であるEitherというデータ型を使ってエラーの原因を突き止める。

sealed trait Either[+E,+A]
case class Left[+E](get: E) extends Either[E,Nothing]
case class Right[+A](get: A) extends Either[Nothing,A]

Optionと同様にEitherにもcaseが二つ定義されているが,違いはどちらにも値が指定されていることである。
おおまかに言えば,Either型は二つのうちのどちらかになる値を表す。
Right(=正しい)コンストラクタを成功のケースに,Leftコンストラクタを失敗のケースに使うのが慣例となっている。
ここでは,Leftの型パラメータにEというErrorを思わせる名前をつけている。

mean関数の例

meanの例をもう一度見てみる。今度はエラーが発生した場合にStringの値を返すことにする。

  def mean(xs: IndexedSeq[Double]): Either[String, Double] =
    if (xs.isEmpty)
      Left("mean of empty list!")
    else
      Right(xs.sum / xs.length)

上記ではLeftに"mean of empty list!"というエラー内容を書いたStringを入れているが,
もう少し詳細な,例えばスタックトレースなどの情報がほしいこともある。
その場合,EitherのLeft側で例外を返すこともできる。

  def safeDiv(x: Int, y: Int): Either[Exception, Int] =
    try Right(x / y)
    catch { case e: Exception => Left(e) }

Optionで行ったように,このスローされた例外を値に変換するTry関数を共通パターンとして抽出することもできる。

  def Try[A](a: => A): Either[Exception, A] =
    try Right(a)
    catch { case e: Exception => Left(e) }
Exercise 4.6
  • Right値を操作するmap, flatMap, orElse, map2をEitherに追加せよ。

まずはmap。Leftならそのまま返し,Rightなら関数を適用する。

    def map[B](f: A => B): Either[E, B] = this match {
      case Left(e) => Left(e)
      case Right(a) => Right(f(a))
    }

実際に動かすとこうなる。

scala> val x: holder2.Either[String,Int] = holder2.Right(3)
x: holder2.Either[String,Int] = Right(3)

scala> val y: holder2.Either[String,Int] = holder2.Left("invalid value!")
y: holder2.Either[String,Int] = Left(invalid value!)

scala> x.map(_ * 2)
res53: holder2.Either[String,Int] = Right(6)

scala> y.map(_ * 2)
res54: holder2.Either[String,Int] = Left(invalid value!)

続いてflatMap。

    def flatMap[EE >: E, B](f: A => Either[EE, B]): Either[EE, B] this match {
      case Left(e) => Left(e)
      case Right(a) => f(a)
    }

続いてorElse。Leftなら引数bを返し,Rightならそのまま返す。

    def orElse[EE >: E, B >: A](b: => Either[EE, B]): Either[EE, B] = this match {
      case Left(e) => b
      case Right(a) => Right(a)
    }

最後にmap2。
前回は引数が二つあったが,今回はtraitに作る関数のため,a map2 bのように呼び出す形になる。

    def map2[EE >: E, B, C](b: Either[EE, B])(f: (A, B) => C): Either[EE, C] =
      for {
        aa <- this
        bb <- b
      } yield (f(aa, bb))
for内包表記

上記の関数を定義すると,Either内でfor内包表記が使えるようになる。
例えば,前節の保険会社のWebアプリケーションの関数を見てみる。

def parseInsuranceRateQuote(age: String, num: String): Either[Exception, Double] =
  for {
    a <- Try { age.toInt }
    tickets <- Try { num.toInt }
  } yield insuranceRateQuote(a, tickets)

これで,エラーが発生した場合に単にNoneが返されるだけでなく,例外に関する情報が返されるようになる。

Exercise 4.7
  • エラーが発生したときに,最初に検出されたエラーを返すsequenceをEitherに実装せよ。

Optionのときと同じように書ける。

    def sequence[E,A](es: List[Either[E,A]]): Either[E,List[A]] = es match {
      case Nil => Right(Nil)
      case h ::	t => h flatMap(hh => sequence(t) map (hh :: _))
    }  

動かすとこんな感じ。

scala> val list = List(x,y,z)
list: List[holder2.Either[String,Int]] = List(Right(1), Right(2), Right(3))

scala> val list2 = List(x,q,z)
list2: List[holder2.Either[String,Int]] = List(Right(1), Left(hogee), Right(3))

scala> holder2.Either.sequence(list)
res55: holder2.Either[String,List[Int]] = Right(List(1, 2, 3))

scala> holder2.Either.sequence(list2)
res56: holder2.Either[String,List[Int]] = Left(hogee)
mkPerson関数

最後に,map2の応用例として,指定された名前と年齢を検証して有効なPersonを生成するmkPerson関数を見てみる。

case class Person(name: Name, age: Age)
sealed class Name(val value: String)
sealed class Age(val value: Int)

def mkName(name: String): Either[String, Name] =
  if (name == "" || name == null) Left("Name is empty.")
  else Right(new Name(name))

def mkAge(age: Int): Either[String, Age] =
  if (age < 0) Left("Age is out of range.")
  else Right(new Age(age))

def mkPerson(name: String, age: String): Either[String, Person] =
  mkName(name).map2(mkAge(age))(Person(_, _))

使ってみるとこんな感じになる。

scala> mkPerson("hoge",1)
res17: holder2.Either[String,Person] = Right(Person(Name@36e43829,Age@152c4495))

scala> mkPerson("hoge",-1)
res18: holder2.Either[String,Person] = Left(Age is out of range.)

scala> mkPerson("",-1)
res19: holder2.Either[String,Person] = Left(Name is empty.)
Exercise 4.8
  • 上の実装では,名前と年齢が両方とも無効でもmap2は一つしかエラーを報告できない。両方のエラーを報告するには何を変更すればよいか。

確かに,両方に無効な値を入れても"Name is empty."としか表示されない。

scala> mkPerson("",-1)
res19: holder2.Either[String,Person] = Left(Name is empty.)

複数のエラーを報告するためには,Either[E, A]のEのところにerrorのリストのようなものを入れれば良い。
解答を見ても,どうやらそんなことを言ってるようだ(英語の読解力が怪しい...)。

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]

まとめ

本章では,純粋関数型のエラー処理の基本原理として,OptionとEitherに焦点を当てて説明した。
例外を通常の値として表し,高階関数を使ってエラーの処理と伝搬に共通するパターンをカプセル化できるようにすることが大きな目的であった。
この新しいツールを使い,例外はどうしても回復不能な状況のためにとっておくこと。
次章では,本章でも少し出てきた非正格がなぜ重要なのか,モジュール性と効率性にどのくらい貢献するのかを詳しく見ていく。

[購入した本] 6/1〜6/22に購入した本リスト

kindle50%オフやら何やらでたくさん買ってしまった。
まだしばらくScala関数型デザインに掛かりそうだが…

  • Code Complete

Code Complete 第2版 上 完全なプログラミングを目指して
Code Complete 第2版 下 完全なプログラミングを目指して

50%オフで思わず買ってしまった。。。ゆっくり読もう。

  • シェルプログラミング実用テクニック

シェルプログラミング実用テクニック (Software Design plus)

割りと業務でシェルスクリプトを書くので。

  • すごいH本

すごいHaskellたのしく学ぼう!

読むかなぁ・・・?

プログラミングコンテスト攻略のためのアルゴリズムとデータ構造

プロコンの方ももうちょっと頑張りたい。

  • Docker

Linuxコンテナー最新ツール Dockerを支える技術(日経BP Next ICT選書) 日経Linux技術解説書
Dockerエキスパート養成読本[活用の基礎と実践ノウハウ満載!] (Software Design plus)

業務で使うので。

  • Jenkins

改訂新版Jenkins実践入門 ――ビルド・テスト・デプロイを自動化する技術 (WEB+DB PRESS plus)

こっちも業務で。買ったばかりなのに改訂新版が出ていた…