2012年5月17日木曜日

Scala Tips / Validation (13) - toValidatation

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

前回はparseIntメソッドなどを用いて、文字列から数値への変換を行いながらフォーマットエラーをValidationとして取り出す方法について説明しました。

次は、アプルケーションの定めた値域に対するチェックを追加する処理を考えてみます。

ScalazではFunction1[T, Boolean]にtoValidationメソッドが追加されているので、その使い心地のチェックも兼ねています。

課題

年齢の入力チェックの関数です。文字列を入力にし、0以上または150以下のInt値に合致する場合はValidation[NonEmptyList[Throwable], Int]のSuccess、それ以外の場合はエラー内容に対応する文字列をIllegalArgumentExceptionに設定したFailureを返します。

if式

普通にif式で書くと以下のようになります。これでも十分ですが、「new IllegalArgumentException」や「failNel」の文言を各判定ごとに書くのが煩雑です。また、ちょっとプログラムっぽい感じなので、できればDSL的に宣言っぽくしたいところです。

def validateAge(age: String): ValidationNEL[Throwable, Int] = {
  age.parseInt.liftFailNel.flatMap { a =>
    if (a < 0) new IllegalArgumentException("年齢が0より小さいです").failNel
    else if (a > 150) new IllegalArgumentException("年齢が150より大きいです").failNel
    else a.successNel
  }
}

validation関数

共通処理をvalidation関数として部品化します。ポイントになるのがFunction1[T, Boolean]のtoValidationメソッドです。関数の評価結果がtrueの場合、関数の引数をValidation[NonEmptyList[Throwable], Int]のSuccessに、エラーの場合は、Failureにエラー情報を設定したものを返す関数を返します。

今回のケースではエラー情報をIllegalArgumentExceptionに設定します。

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

validation関数を使ったvalidateAge関数は以下のようになります。ValidationはモナドなのでflatMapメソッドを使って連結することができます。

def validateAge(age: String): ValidationNEL[Throwable, Int] = {
  age.parseInt.liftFailNel.flatMap {
    x => validate((_: Int) >= 0, "年齢が0より小さいです", x)
  } flatMap {
    x => validate((_: Int) <= 150, "年齢が150より大きいです", x)
  }
}

さらに部分適用を使って、validation関数を関数オブジェクト化することで、以下のようにより簡潔に記述できます。

def validateAge(age: String): ValidationNEL[Throwable, Int] = {
  age.parseInt.liftFailNel.flatMap {
    validate((_: Int) >= 0, "年齢が0より小さいです", _)
  } flatMap {
    validate((_: Int) <= 150, "年齢が150より大きいです", _)
  }
}

for式

Validationはモナドなのでfor式を使って連結することができます。for式を使った場合、余分の変数名を考えないといけないのがちょっと不満ですが、シンプルに書けるのは確かです。

def validateAge(age: String): ValidationNEL[Throwable, Int] = {
  for {
    a <- age.parseInt.liftFailNel
    b <- validate((_: Int) >= 0, "年齢が0より小さいです", a)
    c <- validate((_: Int) <= 150, "年齢が150より大きいです", b)
  } yield c
}

DSL

DSLっぽく宣言的に書くための部品を用意します。

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 validateAge(age: String): ValidationNEL[Throwable, Int] = {
  validates(
    (_: String).parseInt.liftFailNel,
    List(validate((_: Int) >= 0, "年齢が0より小さいです", _),
         validate((_: Int) <= 150, "年齢が150より大きいです",_)),
    age)
}

さらにDSL

さらにDSLを目指して以下の部品を用意します。こういった文法糖衣的なユーティリティの関数を用意するのはよく使うテクニックです。

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

def greaterEqual(value: Int, message: String) = {
  validate((_: Int) >= value, message, (_: Int))
}

def lessEqual(value: Int, message: String) = {
  validate((_: Int) <= value, message, (_: Int))
}

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

この部品を使うと以下のように書くことができます。

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

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

大小比較のgreaterEqual関数、lessEqual関数は、ScalazのOrderを使ってInt型に依存しない汎用的なものにすることができます。

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

ノート

Function1[T, Boolean]のtoValidationメソッドはValidationではなくValicationNELを生成することもあり便利かもしれないと思って考えてみましたが、ValidationNEL[Throwable, T]を標準形とするなら、IllegalArgumentExceptionなどのExceptionを生成する処理が必要となるため結局専用の関数を用意することになり(validation関数)それほど使いどころはなさそうです。

逆にValidationNEL[String, T]でよいケースでは、なかなか便利に使えそうな感触を持ちました。

ValidationNEL[String, T]で処理を進めておいて、最後にValidationNEL[Throwable, T]にするという形にすれば、toValidationメソッドを活かして書くことができるかもしれません。

DSL

toValidation関数を使うだけだと、あまり面白くないので、値域の設定をよりDSLっぽくできるようにしてみました。こういったDSLを作る場合には、関数オブジェクトや部分適用といった関数型ならではの言語機能がとても便利です。こういった細かいところの使い勝手も「DSL指向言語」としては大事ですね。

追記 (2012-06-21)

タイトルを「Scala Tips / Validation (13) - toValidate」から「Scala Tips / Validation (13) - toValidation」に変更しました。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

0 件のコメント:

コメントを投稿