2012年5月18日金曜日

Scala Tips / Validation (14) - オブジェクトの生成

ValidationはScalazが提供する成功/失敗の計算文脈を提供するモナドです。Validationを使ってOptionやEitherと同様の成功/失敗の計算文脈上でのMonadicプログラミングをすることができます。

今回はここまで調べてきた以下の技法を使って、Validationの代表的なユースケースであるオブジェクトの生成について考えます。

課題

文字列のデータから以下のPersonオブジェクトを生成します。

case class Person(name: String, age: Int, address: String)

name, age, addressの各属性は、以下の関数を使って検証します。

  • validateName
  • validateAge
  • validateAddress

この検証がすべて成功した場合のみPersonオブジェクトを生成します。

Personオブジェクトを生成する関数は以下のシグネチャになります。結果はValidationNEL[Throwable, Person]に格納します。

  • f(name: String, age: String, address: String): ValidationNEL[Throwable, Person]

準備

前回(「Validation (13) - toValidate」)に用意した部品です。

def validate[T](f: T => Boolean, message: String, v: T): ValidationNEL[Throwable, T] = {
  v |> f.toValidation(new IllegalArgumentException(message))
}

def validates[A, B](
  f: A => ValidationNEL[Throwable, B],
  g: Seq[B => ValidationNEL[Throwable, B]],
  value: A
): ValidationNEL[Throwable, B] = {
  g.foldLeft(f(value)) { (a, x) =>
    a.flatMap(x)
  }
}

def validatesDef[A, B](
  d: (A => ValidationNEL[Throwable, B],
      Seq[B => ValidationNEL[Throwable, B]]),
  v: A
): ValidationNEL[Throwable, B] = {
  validates(d._1, d._2, v)
}

def intValue = (_: String).parseInt.liftFailNel

def greaterEqual[T: Order](value: T, message: String) = {
  validate(value.lte, message, (_: T))
}

def lessEqual[T: Order](value: T, message: String) = {
  validate(value.gte, message, (_: T))
}

前回に使用した定義です。

val ageDef = (
  intValue,
  List(greaterEqual(0, "年齢が0より小さいです"),
       lessEqual(150, "年齢が150より大きいです")))

def validateAge(age: String): ValidationNEL[Throwable, Int] = {
  validatesDef(ageDef, age)
}

新しい部品と定義

今回は以下の部品を追加します。

def stringValue = (_: String).success[NonEmptyList[Throwable]]

def maxLength[T](conv: T => Int, length: Int, message: String) = {
  validate(conv(_: T) <= length, message, (_: T))
}

def maxStringLength(length: Int, message: String) = {
  maxLength((_: String).length, length, message)
}

def minLength[T](conv: T => Int, length: Int, message: String) = {
  validate(conv(_: T) >= length, message, (_: T))
}

def minStringLength(length: Int, message: String) = {
  minLength((_: String).length, length, message)
}

新しく名前と住所の検証を行う関数を追加しました。ここでは、文字列の長さの検証を行っていますが、実務で使うような場合は文字列のパターンや使って良い文字、NGワードの判定などを行うことになるでしょう。

val nameDef = (
  stringValue,
  List(maxStringLength(10, "名前が長すぎます"),
       minStringLength(2, "名前が短すぎます")))

val addressDef = (
  stringValue,
  List(maxStringLength(100, "住所が長すぎます"),
       minStringLength(10, "住所が短すぎます")))

def validateName(name: String): ValidationNEL[Throwable, String] = {
  validatesDef(nameDef, name)
}

def validateAddress(address: String): ValidationNEL[Throwable, String] = {
  validatesDef(addressDef, address)
}

Personオブジェクトの生成

Validationを使う場合の定番のイディオムがApplicative演算(「Validation (10) - applicative」)です。以下のようにApplicativeを使うと一発で処理を記述することができます。

def f(name: String, age: String, address: String): ValidationNEL[Throwable, Person] = {
  (validateName(name) |@| validateAge(age) |@| validateAddress(address))(Person)
}

ノート

上の処理と同じことを普通に書いていくと以下のようになります。

def f(name: String, age: String, address: String): ValidationNEL[Throwable, Person] = {
  validateName(name) match {
    case Success(na) => validateAge(age) match {
      case Success(ag) => validateAddress(address) match {
        case Success(ad) => Person(na, ag, ad).success
        case Failure(ead) => ead.fail[Person]
      }
      case Failure(eag) => validateAddress(address) match {
        case Success(ad) => eag.fail
        case Failure(ead) => (eag |+| ead).fail
      }
    }
    case Failure(ena) => validateAge(age) match {
      case Success(ag) => validateAddress(address) match {
        case Success(ad) => ena.fail
        case Failure(ead) => (ena |+| ead).fail
      }
      case Failure(eag) => validateAddress(address) match {
        case Success(ad) => (ena |+| eag).fail
        case Failure(ead) => (ena |+| eag |+| ead).fail
      }
    }
  }
}

パラメタが3つなので、2の3乗で8パターンの判定が必要になります。これでもかなり煩雑ですし、単純な処理ではあるものの手作業でコーディングしていくとエラーが混入してしまいそうです。また、パラメタが増えてくると判定パターンが増えてきて破綻してしまうのは明らかです。

こういった共通処理を一つにまとめているのがApplicative(+Monoid)というわけです。関数型プログラミングの威力は、手続き型やオブジェクト指向では共通化、部品化できなかったこういった定型的なアルゴリズムを部品化して再利用できることにあります。そのための道具として圏論(Applicative)や群論(Monoid)といった数学理論が利用されているわけですね。

for式

Applicativeのある所にfor式あり。ということでfor式を使って以下のように書くこともできます。

def f(name: String, age: String, address: String): ValidationNEL[Throwable, Person] = {
  for {
    a <- validateName(name)
    b <- validateAge(age)
    c <- validateAddress(address)
  } yield Person(a, b, c)
}

for式はScalaz ApplicativeではなくScalaモナドをターゲーットにしていますが、Scalaz ApplicativeのオブジェクトはおおむねScalaモナドにもなっているので、一般的にはApplicativeでやっている処理をfor式でも記述することができると考えてもよいでしょう。(「Validation (11) - モナド」で説明したとおり、ValidationはScalaz Monadではないものの、Scalaz ApplicativeかつScalaモナドという、やや特殊な性質のオブジェクトになっています。)

Promiseを使った並列計算などのようにApplicativeとfor式で動きが違うケースもあるので、必ずしも相互変換可能というわけではないので、注意は必要です。

こうやってみていくとfor式はかなり強力ですね。Monadicプログラミング的にも迷ったらfor式という方針で臨むのがよいでしょう。

クロージャ

本文の例は、Scalaのクロージャの記述方式を最大限に利用して、最大限の省略を行っていますが、慣れていない人にはコードゴルフ的なコードともいえます。

省略を行わず丁寧に書くと以下のようになります。

def f(name: String, age: String, address: String): ValidationNEL[Throwable, Person] = {
  (validateName(name) |@| validateAge(age) |@| validateAddress(address))((nm, ag, ad) => Person(nm, ag, ad))
 }

少しだけ省略して以下のように書くこともできます。

def f(name: String, age: String, address: String): ValidationNEL[Throwable, Person] = {
  (validateName(name) |@| validateAge(age) |@| validateAddress(address))(Person(_, _, _))
}

このあたりはコードをたくさん書いて慣れていくしかありません。

タプル

Applicative演算や関数呼び出しを汎用的な枠組みで扱いたい場合に、タプルを使うケースが多々あります。

ついでなので、今回のケースであえてタプルを介在させてみましょう。

Validation (10) - applicative」で説明したように演算子「|@|」で生成されるApplicativeBuilderのtupledメソッドで、Applicative演算の結果を格納したタプルを生成することができます。 タプルは、ValidationNEL[Throwable, Tuple3[String, Int, String]]という形でValidationに格納されることになります。このValidationモナドの文脈上で、mapメソッドを用いてタプルからPersonオブジェクトの生成を行えばよいわけです。

def f(name: String, age: String, address: String): ValidationNEL[Throwable, Person] = {
  (validateName(name) |@| validateAge(age) |@| validateAddress(address)).tupled.map(Person.tupled)
}

Applicative演算でタプルを生成する場合には、演算子「<|**|>」を用いる方法もあります。この場合には以下のようになります。

def f(name: String, age: String, address: String): ValidationNEL[Throwable, Person] = {
  (validateName(name) <|**|> (validateAge(age), validateAddress(address))).map(Person.tupled)
}

なお、tupledメソッドは関数オブジェクトの提供するメソッドで、コンストラクタには使用することができません。今回tupledメソッドを使用できるのは、newによるオブジェクトの生成ではなくて、case classだからです。case classの場合は、内部的にファクトリオブジェクトのメソッドでオブジェクトの生成を行うことになるので、tupledメソッドを使うことができるわけです。

参考

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

0 件のコメント:

コメントを投稿