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

新人SEの学習記録

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

学習記録:Scala関数型デザイン 第4章:例外を使わないエラー処理(続き)

Optionデータ型(続き)

Optionの合成,リフト,例外指向のAPIのラッピング

Optionを使おうとすると,Optionをやり取りするメソッドの呼び出し元をSomeやNoneを処理するように変更しなければならない?
→通常の関数をリフトすれば,Optionに対応する関数にできる。

例えばmap関数では,A => B型の関数を使ってOption[A]型の値を操作してOption[B]を返すことができる。
これはつまり,A => B型の関数を,mapが変換してOption[A] => Option[B]型の関数にしていると考えることもできる。

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

このliftを使って,既存の関数を単一のOption値のコンテキスト内で動作する関数に変換できる。

val absO: Option[Double] => Option[Double] = lift(math.abs)

mathオブジェクトに含まれる関数をOption型の値に対応させるために書き直す必要はなく,
Optionのコンテキストにそのままリフトするだけで済むことがわかる。

別の例として,自動車保険会社のWebサイトのロジックを実装してみる。
以下は,入力フォームの情報の2つの主要項目から,年間自動車保険料を計算する関数である。

def insuranceRateQuote(age: Int, numberOfSpeedingTickets: Int): Double

ユーザは年齢とスピード違反切符の番号をWebフォームから送信し,Webアプリケーション上で上記の関数が呼び出される。
このとき,年齢と切符番号は文字列として届くため,整数への変換が必要となるが,文字列が有効な整数でない場合に失敗してしまう。

scala> "hello".toInt
java.lang.NumberFormatException: For input string: "hello"

例外ベースのAPIであるtoIntをOptionに変換することで,parseInsuranceRateQuote関数を実装できるか調べてみる。
この関数は,年齢とスピード違反切符番号を文字列として受け取り,両方の値の変換に成功したらinsuranceRateQuoteを呼び出す。

def parseInsuranceRateQuote(age: String, numberOfSpeedingTickets: String): Option[Double] = {
  val optAge: Option[Int] = Try(age.toInt)
  val optTickets: Option[Int] = Try(numberOfSpeedingTickets.toInt)
  // insuranceRateQuoteの引数はInt, Intなので,Option[Int]は渡せない
  // insuranceRateQuote(optAge, optTickets)
}

def Try[A](a: => A): Option[A] =
  try Some(a)
  catch { case e: Exception => None }

Try関数は,例外ベースのAPIをOptionベースに変換するための汎用目的の関数である。
aの型として=> Aが指定されており,非正格な引数(=遅延引数)を使用する関数であることがわかる(次章で詳しく説明)。
引数aの評価を遅らせ,aの評価を関数内で行う際に発生する例外をキャッチする仕様になっている。

しかし,このparse...関数には問題が一つある。optAgeとoptTicketsをOption[Int]として解析した後,
現在引数としてInt型の値が定義されているinsuranceRateQuote関数を一旦どのように呼び出すのか?
insuranceRateQuote関数の引数をOption[Int]に変更するのはトラブルの元であるし,そもそも変更出来ない別モジュールの可能性もある。
そこで,insuranceRateQuoteをリフトして,二つのOption値のコンテキストに対応させることにする。

Exercise 4.3

二項関数を使ってOption型の二つの値を結合する総称関数map2を定義せよ。どちらかがNoneなら戻り値もNoneになる。

解答は以下。

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

Option[A]型のaとOption[B]型のbに対し,aの中身aaとbの中身bbを引数に関数fを呼ぶ必要がある。
まず,Optionの中身に対して関数fを適用するにはmapを使うところから考える。

// map関数はA => Bの関数fを引数にOption[B]を返す。
// ここではB => Cの関数bb => f(aa,bb)を引数にOption[C]を返すことになる。
b map (bb => f(aa, bb))

これだとaの中身aaが定義されていない。
型Aのaaを引数にOption[C]を返す必要があるので,ここではflatMapを使う。

// flatMap関数はA => Option[B]の関数を引数にOption[B]を返す。
// ここではA => Option[C]の関数aa => b map (bb => f(aa,bb))を引数にOption[C]を返す。
a flatMap (aa => b map (bb => f(aa, bb)))

map2を定義したところで,parseInsuranceRateQuoteの最終行を以下のように実装できる。

...
map2(optAge, optTickets)(insuranceRateQuote)
}

map2関数により,既存の引数2つの関数を(変更せずに)Optionに対応させることができる。
同様にmap3, map4, map5をどのように定義すればよいかもわかるはずである。

for内包表記

関数のリフトでは,for内包表記がよく用いられる。
この構文はflatMapとmapの呼び出しとして自動的に展開される。

// 元の書き方
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)))

// for内包表記
def map2[A,B,C](a: Option[A], b: Option[B])(f: (A, B) => C): 
Option[C] =
  for {
    aa <- a
    bb <- b
  } yield f(aa, bb)

for内包表記は,aa <- aのようなバインディングと,閉じ括弧に続くyieldで構成される。
yieldでは,その前にあるバインディングの左辺の値をどれでも使用できる。
コンパイラはこれらのバインディングをflatMapの呼び出しに脱糖し,最終的なバインディングとyieldをmapの呼び出しに変換する。