2012年4月27日金曜日

Scala Tips / Validation (10) - applicative

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

今回はapplicative演算です。

課題

以下に示す2つの関数validateNameとvalidateAgeを用いてデータの検証を行います。

def validateName(a: String): ValidationNEL[Throwable, String] = {
  if (a == null) new IllegalArgumentException("null name").failNel
  else if (a.length >= 2) a.success
  else new IllegalArgumentException("bad name: " + a).failNel
}

def validateAge(a: Int): ValidationNEL[Throwable, Int] = {
  if (150 > a && a >= 0) a.success
  else new IllegalArgumentException("bad age: " + a).failNel
}
validateName
正しい名前であることを検証。ここでは2文字以上の文字列を判定。
validteAge
正しい年齢であることを検証。ここでは0以上、150未満のInt値。

どちらの検証にも成功した場合はSuccess[NonEmptyList[Throwable], (String, Int)]、一つでも検証に失敗した場合はFailure[NonEmptyList[Throwable], (String, Int)]を返します。

validateNameとvalidateAgeの両方が失敗の場合には、NonEmptyListに両方のエラー情報が入ります。

flatMapやfor式を使った場合は、最初に失敗したエラー情報のみが返されますが、今回のテーマであるapplicativeの場合は、エラー情報が累積されていくのが特徴です。

(分類の基準)

Java風

Validationから直接値を取り出すことができないためキャストを使っており、その分大変見にくいコーディングになっています。その点を差し引いてもif式での判定は冗長な感じがします。

def validate(name: String, age: Int): ValidationNEL[Throwable, (String, Int)] = {
  val a = validateName(name)
  val b = validateAge(age)
  if (a.isSuccess && b.isSuccess) {
    val a1 = a.asInstanceOf[Success[NonEmptyList[Throwable], String]].a
    val b1 = b.asInstanceOf[Success[NonEmptyList[Throwable], Int]].a
    Success((a1, b1))
  } else if (a.isSuccess) {
    b.asInstanceOf[Failure[NonEmptyList[Throwable], (String, Int)]]
  } else if (b.isSuccess) {
    a.asInstanceOf[Failure[NonEmptyList[Throwable], (String, Int)]]
  } else {
    val a1 = a.asInstanceOf[Failure[NonEmptyList[Throwable], String]].e
    val b1 = b.asInstanceOf[Failure[NonEmptyList[Throwable], Int]].e
    Failure(a1 |+| b1)
  }
}

Scala風

match式を使うとかなりすっきりしました。しかし、match式がネストするとプログラムが複雑化する感じです。この場合は判定対象が2つなのでネストはまだ浅いですが、判定対象が増えてくると大変なことになるのは明らかです。

def validate(name: String, age: Int): ValidationNEL[Throwable, (String, Int)] = {
  validateName(name) match {
    case Success(a) => validateAge(age) match {
      case Success(b) => Success((a, b))
      case Failure(e) => Failure(e)
    }
    case Failure(e1) => validateAge(age) match {
      case Success(b) => Failure(e1)
      case Failure(e2) => Failure(e1 |+| e2)
    }
  }
}

Scala

fold, map, flatMapを使ったよい記述方法はないと思います。「Scala風」のmatch式を使った書き方になります。

Scalaz

applicative演算を使うと以下のようになります。コーディングが簡単な|@|メソッドを使っています。

def validate(name: String, age: Int): ValidationNEL[Throwable, (String, Int)] = {
  (validateName(name) |@| validateAge(age))((_, _))
}

数学記号を使った「⊛」メソッドを使うこともできます。

def validate(name: String, age: Int): ValidationNEL[Throwable, (String, Int)] = {
  (validateName(name) ⊛ validateAge(age))((_, _))
}

applicative演算の引数の個数が2つなので<**>メソッドを使うことができます。

def validate(name: String, age: Int): ValidationNEL[Throwable, (String, Int)] = {
  (validateName(name) <**> validateAge(age))((_, _))
}

どの記述方法を選ぶのかは好みによりますが、いずれにしても「Java風」、「Scala風」と比べて劇的に簡潔になっています。

ノート

ScalazでApplicativeを使う場合の論点は3つあります。

  • Monad未満Functor以上でも一定の性質(Applicative演算が適用可能)を持つオブジェクトを(Applicative演算を用いて)簡潔に操作したい。
  • 複数のモナドを引数に取る演算を行いたい。
  • 複数のモナドから新しいモナドを生成する際の文脈の再構築に関して、定型処理を自動化したい。

今回のテーマであるValidationは、後ろ2つに当てはまります。

Monad未満Functor以上の性質は、Scalazの場合はZipper、ZipStreamが当てはまるようです。ただし、普通はMonadでないApplicativeはあまりないので、一般的にはモナドに対してApplicative演算を行うと考えておいてよいと思います。

「複数のモナドを引数に取る演算を行いたい」については、今回の課題では、Validationモナドの上で個々の要素に対するバリデーション結果から最終的なバリデーション結果を演算するため、Applicative演算が効果的となります。

3番目の「文脈の再構築」での「定型処理」の典型例はモノイドによる加法演算です。Validationの場合には、エラー情報をモノイドで蓄積していくようになっています。今回の例ではNonEmptyListがMonoidなので、ここに対してThrowableが蓄積されていきます。

別の書き方

Either (12) - Applicativeの記述方法」でも取り上げましたが、以下のような記述方法も可能です。これがApplicative演算の本来的な意味に忠実な記法です。

def validate(name: String, age: Int): ValidationNEL[Throwable, (String, Int)] = {
  validateName(name) <*> (validateAge(age) <*> ((x: Int) => (y: String) => (y, x)).success)
}

また、mapメソッドを使って関数を持ち上げる処理を少し簡略化する方法もあります。

def validate(name: String, age: Int): ValidationNEL[Throwable, (String, Int)] = {
  validateName(name) <*> (validateAge(age).map(x => y => (y, x)))
}

いずれにしても、本文で紹介した「|@|」や「<**>」の方が便利なので、普通はそちらの方を使っておくのがよいでしょう。応用によってはこちらの記法で書いた方がよいこともあるので、引き出しにストックしておきたい所です。

タプル

Applicative演算が可能な場合<|*|>メソッドで直接タプルを生成することができます。今回の課題ではこれを使用することもできます。

def validate(name: String, age: Int): ValidationNEL[Throwable, (String, Int)] = {
  (validateName(name) <|*|> validateAge(age))
}
fold

参考に「Scala」的なfoldを使ったコーディングを考えてみました。演算結果がタプルになるとfoldとの相性がわるいので、Listにしています。Listの場合、タプルより型情報の記述力が落ちるので、本文の課題より脆弱なプログラムになります。

def validate(name: String, age: Int): ValidationNEL[Throwable, List[Any]] = {
  List(validateName(name), validateAge(age)).foldLeft((List[Any](), List[Throwable]())) {
    (a, x) => x match {
      case Success(s) => (a._1 :+ s, a._2)
      case Failure(e) => (a._1, a._2 ::: e.list)
    }
  } match {
    case (xs, Nil) => Success(xs)
    case (_, x :: xs) => Failure(nel(x, xs))
  }
}

この場合は、引数の数が2つなので、かなり冗長な感じがしますが、引数が多数になる場合、引数の個数が任意個になる場合には有効なアプローチです。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

2012年4月26日木曜日

Scala Tips / Validation (9) - forで演算を連結2

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

前々回はmapメソッドを用いて行う成功/失敗の文脈を切り替えない演算、flatMapメソッドを用いて行う成功/失敗の文脈を切り替える演算をfor式で記述する方法について説明しました。また、前回はより複雑な例をflatMapメソッドとfor式で記述しました。

いずれの場合も、flatMapメソッドの方がfor式よりも簡潔に記述できました。

今回は、さらに問題を複雑にしてfor式の方が簡潔になるケースについて考えます。

課題

以下に示す3つの関数mul101、toText、toLabelを組合わせてValidationNEL[Throwable, Int]をValidationNEL[Throwable, String]に変換する演算を考えます。

def mul101(a: Int): ValidationNEL[Throwable, Int] = {
  if (a >= 0) (a * 101).success
  else new IllegalArgumentException("less than 0: " + a).failNel
}

def toText(a: Int): ValidationNEL[Throwable, String] = {
  if (a % 2 == 0) a.toString.success
  else new IllegalArgumentException("not even: " + a).failNel
}  

def toLabel(a: String): ValidationNEL[Throwable, String] = {
  if (a.length < 5) ("Success:" + a.toString).success
  else new IllegalArgumentException("large length: " + a).failNel
}
mul101
Intを101倍する。0未満の場合はエラー。
toText
IntをStringにする。奇数の場合はエラー。
toLabel
Stringを整形する。文字数が5以上の場合はエラー。

いずれの関数も、入力パラメタの値によってエラーになるところがポイントです。これらの関数を組合わせて、成功の文脈と失敗の文脈を切り替える処理行うわけですが、正常系のアルゴリズム(成功の文脈)を簡潔に分かりやすく記述することを目指します。

ここまでは前回と同じです。

今回は、これらの演算を組み合わせた上で、mul101関数の結果、toText関数の結果、toLabel関数の結果を一つの文字列にまとめる処理を行います。

(分類の基準)

Java風

中核のロジックは「"%s -> %s -> %s -> %s".format(b, d, f, h)」です。ボイラープレートのコードに埋もれてしまい、前回との違いがよく分かりません。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  if (a.isSuccess) {
    val b = a.asInstanceOf[Success[NonEmptyList[Throwable], Int]].a
    val c = mul101(b)
    if (c.isSuccess) {
      val d = c.asInstanceOf[Success[NonEmptyList[Throwable], Int]].a
      val e = toText(d)
      if (e.isSuccess) {
        val f = e.asInstanceOf[Success[NonEmptyList[Throwable], String]].a
        val g = toLabel(f)
        if (g.isSuccess) {
          val h = g.asInstanceOf[Success[NonEmptyList[Throwable], String]].a
          Success("%s -> %s -> %s -> %s".format(b, d, f, h))
        } else {
          g.asInstanceOf[ValidationNEL[Throwable, String]]
        }
      } else {
        e.asInstanceOf[ValidationNEL[Throwable, String]]
      }
    } else {
      c.asInstanceOf[ValidationNEL[Throwable, String]]
    }
  } else {
    a.asInstanceOf[ValidationNEL[Throwable, String]]
  }
}

Scala風

match式を使う場合も、中核ロジックがボイラープレートコードに埋もれてしまいます。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  a match {
    case Success(b) => {
      mul101(b) match {
        case Success(c) => {
          toText(c) match {
            case Success(d) => {
              toLabel(d) match {
                case Success(e) => Success("%s -> %s -> %s -> %s".format(b, c, d, e))
                case Failure(g) => Failure(g)
              }
            }
            case Failure(h) => Failure(h)
          }
        }
        case Failure(i) => Failure(i)
      }
    }
    case Failure(j) => Failure(j)
  }
}

Scala

flatMapメソッドを用いて書くと以下のようになります。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  a.flatMap(b => mul101(b).flatMap(c => toText(c).flatMap(d => toLabel(d).map(e =>
    "%s -> %s -> %s -> %s".format(b, c, d, e)))))
}

前回との違いは、 中核ロジックである「"%s -> %s -> %s -> %s".format(b, c, d, e)」が、最初の入力、mul101関数の結果、toText関数の結果、toLabel関数の結果を使用することです。前回は最後に実行するtoLabel関数の結果のみを使っていました。

このため、最初の入力、mul101関数の結果、toText関数の結果、toLabel関数の結果を変数に覚えておき、後ろの段で参照する必要があります。これを実現するためには、各関数の結果を変数に設定するようにすると同時に、flatMapメソッド内で実行する関数の中で次のflapMapメソッドを呼ぶというように、flatMapメソッドをネストして実行します。

flatMapの実行結果のみを次のflatMapに渡すだけであれば、前回のようにパイプライン的につないでいけばよいのですが、前段の処理結果を後段で参照する必要がある場合は、flatMapをネストさせていかなければならないわけです。変数を設定する処理、ネストしていること、を一行にまとめてしまうと可読性が悪いので、これを整形すると以下のようになります。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  a.flatMap(b =>
    mul101(b).flatMap(c =>
      toText(c).flatMap(d =>
        toLabel(d).map(e =>
          "%s -> %s -> %s -> %s".format(b, c, d, e)))))
}

また、以下のようにも書けます。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  a.flatMap { b =>
    mul101(b).flatMap { c =>
      toText(c).flatMap { d =>
        toLabel(d).map { e =>
          "%s -> %s -> %s -> %s".format(b, c, d, e)
        }
      }
    }
  }
}

このようになってくるとflatMapメソッドであっても、簡潔に記述という感じではなくなってきました。

Scalaz

Scalaz的に>>=メソッドを使うと以下のようになります。Validationに対して>>=メソッドを使う場合は「import Validation.Monad._」のおまじないをしないといけないのが難点です。(「Validation (5) - flatMap」参照)

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  import Validation.Monad._

  a >>= (b => mul101(b) >>= (c => toText(c) >>= (d => toLabel(d).map(e =>
    "%s -> %s -> %s -> %s".format(b, c, d, e)))))
}
def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  import Validation.Monad._

  a >>= (b =>
    mul101(b) >>= (c =>
      toText(c) >>= (d =>
        toLabel(d).map(e =>
          "%s -> %s -> %s -> %s".format(b, c, d, e)))))
}
def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  import Validation.Monad._

  a >>= { b =>
    mul101(b) >>= { c =>
      toText(c) >>= { d =>
        toLabel(d).map {e =>
          "%s -> %s -> %s -> %s".format(b, c, d, e)
        }
      }
    }
  }
}

for式

さて、本題のfor式で書くと以下のようになります。for式は前回の場合と複雑度はほとんど変わりません。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  for {
    b <- a
    c <- mul101(b)
    d <- toText(c)
    e <- toLabel(d)
  } yield "%s -> %s -> %s -> %s".format(b, c, d, e)
}

より普通のfor式っぽく以下のように書くこともできます。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  for (b <- a; c <- mul101(b); d <- toText(c); e <- toLabel(d)) yield {
    "%s -> %s -> %s -> %s".format(b, c, d, e)
  }
}

ノート

flatMapで関数を接続する演算で、前段で実行した各関数の結果を直後ではない後段で参照するようなケースでは、(1)関数の実行結果を覚えておく変数(クロージャの引数)の導入、(2)flatMapをネストで実行、を行う必要があるため、プログラムが複雑化します。

一方、for式はこのような使い方でも使い勝手は全く一緒です。

このため、今回の課題ではfor式の方が簡潔に記述することができます。

Monadicプログラミングをしていくと、flatMapを多段につないでいくことになりますが、処理が複雑化していくと今回の課題のようなネスト構造が登場して、プログラムの取り回しに苦労するようになります。これがMonadicプログラミングで頻出のパターンになので、これに対する文法糖衣としてfor式が用意されているわけですね。

簡単なパイプライン的な処理の場合はflatMapや>>=を使い、複雑になりそうな場合はfor式を選択するという形で、適材適所で使い分けをしていきたいところです。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

2012年4月25日水曜日

Scala Tips / Validation (8) - forで演算の連結

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

前回はmapメソッドを用いて行う成功/失敗の文脈を切り替えない演算、flatMapメソッドを用いて行う成功/失敗の文脈を切り替える演算をfor式で記述する方法について説明しました。

for式も悪くないのですが、簡単な演算だとmapメソッドやflatMapメソッドを使う方がより簡潔な記述になりました。

今回は、もう少し複雑な演算を考えてみます。

課題

以下に示す3つの関数mul101、toText、toLabelを組合わせてValidationNEL[Throwable, Int]をValidationNEL[Throwable, String]に変換する演算を考えます。

def mul101(a: Int): ValidationNEL[Throwable, Int] = {
  if (a >= 0) (a * 101).success
  else new IllegalArgumentException("less than 0: " + a).failNel
}

def toText(a: Int): ValidationNEL[Throwable, String] = {
  if (a % 2 == 0) a.toString.success
  else new IllegalArgumentException("not even: " + a).failNel
}  

def toLabel(a: String): ValidationNEL[Throwable, String] = {
  if (a.length < 5) ("Success:" + a.toString).success
  else new IllegalArgumentException("large length: " + a).failNel
}
mul101
Intを101倍する。0未満の場合はエラー。
toText
IntをStringにする。奇数の場合はエラー。
toLabel
Stringを整形する。文字数が5以上の場合はエラー。

いずれの関数も、入力パラメタの値によってエラーになるところがポイントです。これらの関数を組合わせて、成功の文脈と失敗の文脈を切り替える処理を、正常系のアルゴリズム(成功の文脈)を簡潔に分かりやすく記述することを目指します。

(分類の基準)

Java風

キャストが多くなってしまうのはValidationにSuccessやFailureの値を直接取ってこれる機能がないのが原因ですが、かなり込み入ったコーディングになります。前回の簡単な処理ぐらいであれば許容範囲ですが、ちょっとロジックが複雑になると耐えられないコーディングになってしまいます。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  if (a.isSuccess) {
    val b = a.asInstanceOf[Success[NonEmptyList[Throwable], Int]].a
    val c = mul101(b)
    if (c.isSuccess) {
      val d = c.asInstanceOf[Success[NonEmptyList[Throwable], Int]].a
      val e = toText(d)
      if (e.isSuccess) {
        val f = e.asInstanceOf[Success[NonEmptyList[Throwable], String]].a
        val g = toLabel(f)
        if (g.isSuccess) {
          val h = g.asInstanceOf[Success[NonEmptyList[Throwable], String]].a
          Success(h)
        } else {
          g.asInstanceOf[ValidationNEL[Throwable, String]]
        }
      } else {
        e.asInstanceOf[ValidationNEL[Throwable, String]]
      }
    } else {
      c.asInstanceOf[ValidationNEL[Throwable, String]]
    }
  } else {
    a.asInstanceOf[ValidationNEL[Throwable, String]]
  }
}

Scala風

match式を使うとそれなりのコーディングにはなりますが、かなり面倒なコーディングであることは変わりません。ネストが込み入っているので全体の見通しが悪くなりますし、ボイラープレート的なコードでメインロジックが埋もれてしまっています。エラーハンドリングのコードが正常処理と同じ重み付けで全体の半分を閉めてしまうのも感心しないところです。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  a match {
    case Success(b) => {
      mul101(b) match {
        case Success(c) => {
          toText(c) match {
            case Success(d) => {
              toLabel(d) match {
                case Success(e) => Success(e)
                case Failure(g) => Failure(g)
              }
            }
            case Failure(h) => Failure(h)
          }
        }
        case Failure(i) => Failure(i)
      }
    }
    case Failure(j) => Failure(j)
  }
}

このコードはパターンマッチングは使っていますが、手続き型(OOP含む)の典型的なコーディングになります。

これではたまらないので、エラー処理に例外機構を用いて、正常系処理のコードを簡潔に保つのがOOPで一般に用いられている方法です。ただし、その場合でもシステムエラーもアプリケーションエラーも一律にエラー終了にするといったエラー処理はよいのですが、アプリケーションエラーをアプリケーションロジックで扱うといった用途とは相性が悪いので、コーディング上の工夫が必要になってきます。

Scala

ScalaではflatMapを用いて処理を簡潔に記述するのが普通のコーディングスタイルです。これはValidationがモナドであるために可能になっています。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  a.flatMap(mul101).flatMap(toText).flatMap(toLabel)
}

Scalaz

Scalaz的に>>=メソッドを使うと以下のようになります。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  import Validation.Monad._

  a >>= mul101 >>= toText >>= toLabel
}

for式

さて、本題のfor式で書くと以下のようになります。「Scala」、「Scalaz」と比べると若干冗長な気もしますが、「Java風」、「Scala風」と比べると比較にならないほど簡潔に記述することができます。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  for {
    b <- a
    c <- mul101(b)
    d <- toText(c)
    e <- toLabel(d)
  } yield e
}

より普通のfor式っぽく以下のように書くこともできます。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  for (b <- a; c <- mul101(b); d <- toText(c); e <- toLabel(d)) yield {
    e
  }
}

ノート

for式で簡潔に書ける例を考えてみたのですが、結局flatMapの方がもっと簡潔に書けてしまいました。Monadicプログラミングに慣れてくると、mapやflatMapを好むようになりますが、この辺の事情によりますね。

ただ、for式が「Java風」や「Scala風」よりも圧倒的に簡潔なのは確かなので、flatMapでの実現方法が思いつかない場合でも、for式を使う方向で考えていくとよいでしょう。

今回のように演算を単純につないでいく用途ではflatMapの方が便利ですが、そうでない場合はfor式の方が便利なケースがあります。次回はそのケースを取り上げたいと思います。

追記 (2011-04-26)

>>=メソッドを使うのに「 import Validation.Monad._」が抜けていたので追加しました。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

2012年4月24日火曜日

Scala Tips / Validation (7) - for

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

今回は「Option (7)」や「Either (7) - for」で扱った課題のValidation版を考えてみます。

Validation (4) - map」で成功/失敗の文脈を切り替えない以下の演算をmapで行う方法:

条件結果
Validation[A, B]がSuccess[A, B]Success[A, C]
Validation[A, B]がFailure[A, B]Failure[A, C]

Validation (5) - flatMap」で成功/失敗の文脈を切り替える以下の演算をflatMapで行う方法について説明しました。

条件結果
Validation[A, B]がSuccess[A, B]でBに有効な値が入っているSuccess[A, C]
Validation[A, B]がSuccess[A, B]でBに無効な値が入っているFailure[A, C]
Validation[A, B]がFailure[A, B]Failure[A, C]

大枠ではB→Cの演算を行いたいわけですが、これをfor式を用いてValidation[A, B→C]の文脈の上で行うわけです。

今回はこの2つの演算をfor式を使って書いてみます。

以下ではValidation[NonEmptyList[Throwable], Int]をValidation[NonEmptyList[Throwable], String]に変換するプログラムを考えます。なお、Validation[NonEmptyList[Throwable], Int]はValidationNEL[Throwable, Int]と同等なので、可能な場合はValidationNELの方の表記を用います。

文脈切替なし

Validation (4) - map」で取り上げた成功の文脈と失敗の文脈の切り替えが発生しない演算です。以下の表に示す演算になります。

条件結果
Validation[A, B]がSuccess[A, B]Success[A, C]
Validation[A, B]がFailure[A, B]Failure[A, C]

ValidationNEL[Throwable, Int]からValidation[Throwable, String]への変換は以下のようになります。mapメソッドと同等の処理をfor式で素直に記述できます。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  for (b <- a) yield {
    b.toString
  }
}

文脈切替あり

Validation (5) - flatMap」で取り上げた成功の文脈と失敗の文脈の切り替えが発生する演算です。以下の表に示す演算になります。

条件結果
Validation[A, B]がSuccess[A, B]でBに有効な値が入っているSuccess[A, C]
Validation[A, B]がSuccess[A, B]でBに無効な値が入っているFailure[A, C]
Validation[A, B]がFailure[A, B]Failure[A, C]

Intは0以上のものが有効という条件付きのValidationNEL[Throwable, Int]からValidation[Throwable, String]への変換は以下のようになります。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  def g(c: ValidationNEL[Throwable, Int]) = {
    c.flatMap { d =>
      if (d >= 0) c
      else new IllegalArgumentException(d.toString).failNel
    }
  }

  for (b <- g(a)) yield {
    b.toString
  }
}

Optionの場合と違ってValidationではif句で成功文脈から失敗文脈への切り替え条件を指定することはできません。ValidationはwithFilterメソッド、filterメソッドを持っていないためです。

そこで、ジェネレータに指定する前に成功文脈から失敗文脈への切り替え判定をしておくようにプログラミングしてみました。この処理をfor式内に直接記述するとプログラムの見通しが悪くなるので、ローカル関数を作成し、これを呼び出すようにしています。

プログラムの見通しはかなり悪くなってしまっていて、ちょっと無理がある感じですね。このためValidationを計算文脈として成功文脈から失敗文脈への切り替えを持つ処理をfor式で書くのはあまり得策ではないようです。

ノート

for式はモナドの文法糖衣で、うまくはまると便利なのですが、成功文脈から失敗文脈への切り替えはうまくハンドリングできない感じです。

今回のような簡単な例だとflatMapメソッドを直接使ったほうが簡潔で便利です。

ただし、計算文脈上で多段の計算を行う場合はfor式が便利なのは確かで、「成功文脈から失敗文脈への切り替え」の不細工さを勘案してもfor式を選ぶ方が良いケースもありそうです。このあたりの取捨選択は追々考えていく予定です。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

2012年4月23日月曜日

Scala Tips / Validation (6) - Exception

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

前回はValidationから値を取り出す際に、以下の表の演算を行う方法について考えました。「Option (10) - Exception」や「Either (5) - Exception」で扱った課題のValidation版です。

条件結果
Validation[A, B]がSuccess[A, B]でBに有効な値が入っているSuccess[A, C]
Validation[A, B]がSuccess[A, B]でBに無効な値が入っているFailure[A, C]
Validation[A, B]がFailure[A, B]Failure[A, C]

今回は「Success[A, B]に無効な値が入っている」の判定で例外を使用するケースです。

以下では、ValidationNEL[Throwable, String]からValidationNEL[Throwable, Int]への変換を例に考えます。Validationに入っているStringがIntに変換できる場合は有効となり、Success[NonEmptyList[Throwable], Int]が処理結果となります。一方、変換できない場合は無効となりFailure[NonEmptyList[Throwable], Int]が処理結果となります。

ここで、StringからIntへの変換ができない事の判定に例外を使用します。(String#toIntメソッドがNumberFormatExceptionをスロー。)

(分類の基準)

Java風

if式でValidation#isSuccessを使って成功の有無を判定します。

文字列が整数値に合致しなかった場合String#toIntメソッドがeNumberFormatExceptionをスローするので、これをtry/catch文でキャッチしています。

引数がFailureの場合とNumberFormatExceptionをキャッチした時がFailure(NonEmptyList(Throwable))、toIntで整数値が得られた場合はSuccess(Int)が演算結果となります。

def f(a: ValidationNEL[Throwable, String]): ValidationNEL[Throwable, Int] = {
  if (a.isSuccess) {
    val s = a.asInstanceOf[Success[NonEmptyList[Throwable], String]]
    try {
      Success(s.a.toInt)
    } catch {
      case e: NumberFormatException => {
        Failure(NonEmptyList(e))
      }
    }
  } else {
    a.asInstanceOf[ValidationNEL[Throwable, Int]]
  }
}

Scala風

match式を使うと以下のようになります。try/catch文が入るとプログラムの見通しはJava風とそれほど変わらなくなります。

def f(a: ValidationNEL[Throwable, String]): ValidationNEL[Throwable, Int] = {
  a match {
    case Success(s) => {
      try {
        Success(s.toInt)
      } catch {
        case e: NumberFormatException => {
          Failure(NonEmptyList(e))
        }
      }
    }
    case Failure(e) => Failure(e)
  }
}

Scala

ValidationはScalazの機能ですが、ここではScala的なプログラミングをしてみます。

Either (5) - Exception」で説明したように、mapメソッドを使うのは得策ではないので、flatMapメソッドを用います。flatMapメソッドを使うことで、成功の文脈から失敗の文脈への切り替えを行うことができるようになります。

def f(a: ValidationNEL[Throwable, String]): ValidationNEL[Throwable, Int] = {
  a.flatMap { b =>
    try {
      Success(b.toInt)
    } catch {
      case e: NumberFormatException => {
        Failure(NonEmptyList(e))
      }
    }
  }
}

処理が成功した場合は「Success(b.toInt)」で成功の文脈を維持、例外が発生した場合は「Failure(NonEmptyList(e))」で失敗の文脈に切り替えます。

scala.util.control.Exception

例外をキャッチしてEitherやOptionに変換する機能がオブジェクトscala.util.control.Exceptionに用意されています。よく使うのがcatching関数とallCatch関数です。

catching関数は、キャッチする例外を列挙して指定することができます。catching関数から返されるCatchオブジェクトのeitherメソッドで例外の発生をEitherに変換することができます。

ここで得られたeitherを「Either (20) - Validationと相互変換」で説明した方法でValidationに変換します。

import scala.util.control.Exception._

def f(a: ValidationNEL[Throwable, String]): ValidationNEL[Throwable, Int] = {
  a.flatMap { b =>
    catching(classOf[NumberFormatException]) either {
      b.toInt
    } fold (e => Failure(NonEmptyList(e)), Success(_))
  }
}

Either[Throwable, Int]をValidation[Throwable, Int]に変換する場合はeitherメソッドの値をそのまま使えばよいのですが、ThrowableをNonEmptyList[Throwable]に変換しなければならないので、Either#foldメソッドを用いています。

allCatch

多くの場合はNumberFormatException以外の例外が発生しても関数のエラーとして返して大丈夫だと思われるので、すべての例外をキャッチするallCatching関数を使う方法も有力です。

import scala.util.control.Exception._

def f(a: ValidationNEL[Throwable, String]): ValidationNEL[Throwable, Int] = {
  a.flatMap { b =>
    allCatch either {
      b.toInt
    } fold (e => Failure(NonEmptyList(e)), Success(_))
  }
}

ここで得られたeitherを「Either (20) - Validationと相互変換」で説明した方法でValidationに変換します。

Scalaz

「Scala」と基本的な構造は同じですが、Scalazの機能を利用するとよりコンパクトに記述することができます。

def f(a: ValidationNEL[Throwable, String]): ValidationNEL[Throwable, Int] = {
  a >>= { b =>
    try {
      Success(b.toInt)
    } catch {
      case e: NumberFormatException => {
        e.failNel
      }
    }
  }
}

flatMapメソッドを>>=メソッドで、「Failure(NonEmptyList(e))」を「e.failNel」で記述しました。

scala.util.control.Exception

cathingメソッドで例外をハンドリングする場合は、以下のようになります。

import scala.util.control.Exception._

def f(a: ValidationNEL[Throwable, String]): ValidationNEL[Throwable, Int] = {
  a >>= { b =>
    catching(classOf[NumberFormatException]) either {
      b.toInt
    } fold (_.failNel, _.success)
  }
}

Either#foldメソッドを使う際に「fold(_.failNel, _.success)」とコンパクトに記述できるのがScalaz的です。

allCatch

allCatchのScalaz版は以下のようになります。

import scala.util.control.Exception._

def f(a: ValidationNEL[Throwable, String]): ValidationNEL[Throwable, Int] = {
  a >>= { b =>
    allCatch either {
      b.toInt
    } fold (_.failNel, _.success)
  }
}

ノート

scala.util.control.Exceptionのcatchingメソッド、allCatchメソッドで返されるCatchオブジェクトのeitherメソッドのシグネチャがThrowable固定になっています。

このため、catchingやallCatchを使用する場合、ThrowableではなくExceptionを例外クラスとして使用すると「Either (5) - Exception」で説明したように、色々なところでキャストが必要となりかなり不便です。

そこで、例外クラスを統一的に扱う場合はThrowableを使うのがイディオムとなっています。

今回のValidationの例では、この点を見越してThrowableを使っているので、処理をスムーズに記述できています。

追記(2012-06-21)

ValidationをScalazのMonadとして>>=メソッドを使うには、「Validation (11) - モナド」で説明したように「import Validation.Monad._」のおまじないが必要です。このため「Scalaz」の項で採用している>>=メソッドはこの場合はあまり便利ではありません。素直にflatMapメソッドを使う方がよいでしょう。

throwsメソッド

この記事を書いた時点ではscala.util.control.ExceptionのallCatchを使うのがベストだと思っていたのですが、「Validation (32) - Function0」の記事にあるように「Function0[T]」(「() => T」)のthrowsメソッドを使う方がより便利です。

例外を細かくキャッチし分けたい場合は、引き続きscala.util.control.Exceptionのcatchingが有効です。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

2012年4月20日金曜日

Scala Tips / Validation (5) - flatMap

ValidationはScalazが提供する成功/失敗の計算文脈を提供するモナドです。

Validationを使ってOptionと同様の成功/失敗の計算文脈上でのMonadicプログラミングをすることができます。

今回は以下の処理、アプリケーションの意図で成功の文脈を失敗の文脈に切り替えるためのMonadic演算についてみていきます。「Option(6)」や「Either(2)」で扱った課題のValidation版です。

条件結果
Validation[A, B]がSuccess[A, B]でBに有効な値が入っているSuccess[A, C]
Validation[A, B]がSuccess[A, B]でBに無効な値が入っているFailure[A, C]
Validation[A, B]がFailure[A, B]Failure[A, C]

大枠ではB→Cの演算を行いたいわけですが、これをValidation[A, B→C]の文脈の上で行うわけです。この時、Validation[A, B]がSuccess[A, B]であっても「B」が無効な値である場合には、Failure[A, C]にすることで、成功の文脈から失敗の文脈へ切り替えます。

以下ではValidation[NonEmptyList[Throwable], Int]をValidation[NonEmptyList[Throwable], String]に変換するプログラムを考えます。なお、Validation[NonEmptyList[Throwable], Int]はValidationNEL[Throwable, Int]と同等なので、可能な場合はValidationNELの方の表記を用います。ただし、Intは0以上のものが有効という条件を追加します。Validation(Success)に入っているIntが0以上の場合、Success[NonEmptyList[Throwable], String]が処理結果となります。一方、0未満の場合は無効となりFailure[NonEmptyList[Throwable], String]が処理結果となります。

(分類の基準)

Java風

if式でValidation#isSuccessメソッドを使ってSuccessとFailureの判定をして、処理を切り分ける事ができます。Successの場合に、値の有効判定を行って、無効(0未満)だった場合はFailureを返します。Success、Failure共キャスト(asInstanceOf)を使うことになるので避けたい用法です。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  if (a.isSuccess) {
    val s = a.asInstanceOf[Success[NonEmptyList[Throwable], Int]]
    if (s.a >= 0) {
      Success(s.a.toString)
    } else {
      Failure(NonEmptyList(new IllegalArgumentException("value: " + s.a)))
    }
  } else {
    a.asInstanceOf[ValidationNEL[Throwable, String]]
  }
}

Scala風

match式を使うとSuccessとFailureのパターンマッチングで綺麗に書くことができます。Successの場合に、値の有効判定を行って、無効(0未満)だった場合はFailureを返します。ただし、Scala的には「Failure(b)の場合はFailure(b)」というロジックを書くのが悔しいところです。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  a match {
    case Success(s) => if (s >= 0) {
      Success(s.toString)
    } else {
      Failure(NonEmptyList(new IllegalArgumentException("value: " + s)))
    }
    case Failure(e) => Failure(e)
  }
}

Scala

Option」や「Either」と同様に成功の文脈から失敗の文脈の切替えを行うにはflatMapメソッドを使用します。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  a.flatMap { s =>
    if (s >= 0) {
      Success(s.toString)
    } else {
      Failure(NonEmptyList(new IllegalArgumentException("value: " + s)))
    }
  }
}

失敗の文脈であるFailureの場合は自動的に引き継がれるのがミソです。

Scalaz

Scalazらしい書き方として、flatMapメソッドと同等の機能を持つ>>=メソッドやその別名の「∗」(Unicode 2217)を使うことができます。

import Validation.Monad._

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  a >>= { s =>
    if (s >= 0) s.toString.success
    else new IllegalArgumentException("value: " + s).failNel
  }
}
import Validation.Monad._

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  a ∗ { s =>
    if (s >= 0) s.toString.success
    else new IllegalArgumentException("value: " + s).failNel
  }
}

だたし、いずれも「Validation.Monad._」のインポートが必要となるため、無理をして使う必要はないでしょう。

ノート

Validationで>>=メソッドを使うために「Validation.Monad._」のインポートをすると、Validationに対するApplicativeな動作でFailureのMonoidが積算されないという副作用がでてきます。このメカニズムは、以下のページに詳しい解説がありました。

Validationを使う効用の一つがApplicativeな処理でFailureのMonoidが積算されるという点にあるので、このメリットを放棄するのは得策ではありませんし、デフォルトの動作を暗黙的な形で変えてしまうと原因不明のバグが起きやすくなってしまうので、「Validation.Monad._」のインポートを使う用法はできるだけ避けるのがよいでしょう。

推測ですが、「Validation.Monad._」をめぐる仕様は、Validationに対するApplicativeな動作でFailureの値を:

  • 複数のFailureがある場合に、Monoidとして積算する
  • 複数のFailureがある場合に、そのうちの一つを使う

という仕様上の選択を苦慮した結果かもしれません。

現実装では、Validationの>>=メソッドに対して選択される型クラスBindのValidation用インスタンスは前者の振舞いをします。一方、型クラスMonoidのValidation用インスタンスは後者の振舞いをします。

型クラスインスタンスの解決では、型クラスMonoidの方が型クラスBindより優先度が高いので、この優先度の差を利用して、デフォルト仕様を型クラスBind、カスタマイズ仕様を型クラスMonadで実現してみたのではないかと推測します。

このあたりは、Scalaz 7で導入されるというTagged Typeで解決されるのかな。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

2012年4月19日木曜日

Scala Tips / Validation (4) - map

ValidationはScalazが提供する成功/失敗の計算文脈を提供するモナドです。

Validationを使ってOptionと同様の成功/失敗の計算文脈上でのMonadicプログラミングをすることができます。

今回は「Option(3)」や「Either」で扱った課題のValidation版を考えてみます。

条件結果
Validation[NonEmptyList[Throwable], B]がSuccess[NonEmptyList[Throwable], B]Success[NonEmptyList[Throwable], C]
Validation[NonEmptyList[Throwable], B]がFailure[NonEmptyList[Throwable], B]Failure[NonEmptyList[Throwable], C]

大枠ではB→Cの演算を行いたいわけですが、これをValidation[NonEmptyList[Throwable], B→C]の文脈の上で行うわけです。

以下ではValidation[NonEmptyList[Throwable], Int]をValidation[NonEmptyList[Throwable], String]に変換するプログラムを考えます。なお、Validation[NonEmptyList[Throwable], Int]はValidationNEL[Throwable, Int]と同等なので、可能な場合はValidationNELの方の表記を用います。

(分類の基準)

Java風

if式でValidation#isSuccessメソッドを使ってSuccessとFailureの判定をして、処理を切り分ける事ができます。Failureの場合にキャスト(asInstanceOf)を使うことになるので避けたい用法です。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  if (a.isSuccess) {
    Success(a.value.toString)
  } else {
    a.asInstanceOf[ValidationNEL[Throwable, String]]
  }
}

Scala風

match式を使うとSuccessとFailureのパターンマッチングで綺麗に書くことができます。ただし、Scala的には「Failure(b)の場合はFailure(b)」というロジックを書くのが悔しい。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  a match {
    case Success(b) => Success(b.toString)
    case Failure(b) => Failure(b)
  }
}

Scala

Option」や「Either」と同様に成功の文脈上で値の変換を行うにはmapメソッドを使用します。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  a.map(_.toString)
}

失敗の文脈であるFailureの場合は自動的に引き継がれるのがミソです。

Scalaz

Scalazでは、mapメソッドの別名として「∘」(Unicode 2218)を使用することができます。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  a ∘ (_.toString)
}

ただし、flatMap「∗」(Unicode 2217)やcontramap「∙」(Unicode 2219)と紛らわしいのですし、記号の入力も簡単ではないので、無理をして使うほどのことはなさそうです。

ノート

ValidationとOptionとの違いは、Validationはエラー時の情報を保持しておける点です。

Eitherも右側を成功、左側を失敗という運用でエラー情報を保持する運用ができますが(「Either」, 「Either(2) - flatMap」)、この運用とほとんど同じことができます。

EitherとValidationの違いは以下の点です。

  • EitherはLeft/Rightという汎用的なメカニズムを運用で成功/失敗の文脈として使用しているが、ValidationはSuccess/Failureとして明に成功/失敗の文脈の提供している。
  • Validationはエラー側の情報がモノイドとなっており、エラー情報を追加していくメカニズムを持っている。
記号

Scalazでは、mapメソッドの別名として「∘」(Unicode 2218)、flatMap「∗」(Unicode 2217)やcontramap「∙」(Unicode 2219)といった記号を用いることができることを紹介しました。フォントの関係で「∗」「∘」「∙」の区別がつきにくいのでmapに関しては個人的には使うつもりはありませんが、読めるようになっておく必要はあります。

Scalazで使用できる記号は以下のページが詳しいです。

また、Scalazの記号を入力するためのキー入力のカスタマイズ方法もあるようです。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

2012年4月18日水曜日

Scala Tips / Validation (3) - NonEmptyList

NonEmptyListは、Scalazの提供するデータ構造で、名前から分かるように空であることがないリストです。また、Listと同様にモノイドの性質を持っています。

NonEmptyListは、空でないことが保証されているので、データ構造の持つ値域がより明確になります。具体的には、リストに対する空判定が必要でないことが、プログラムから明確に読み取れるようになります。

ValidationのFailureは、格納するオブジェクトがモノイドの場合、エラー情報を積算していけるようになっています。Listを使ってもよいのですが、Failureを使う場合、エラーが必ず一つ以上あることが前提なので、空でないことが保証されているリストであるNonEmptyListを使うのがより適切です。

そのような事情もあり、ValidationはFailure側にNonEmptyListを使うのが定番のイディオムになっています。

今回は、NonEmptyListを使うValidationの生成方法をまとめてみました。

Failure側はThrowableのNonEmptyList、Success側はIntのValidationの生成方法について考えます。

Failure

正攻法

Failureを生成する正攻法は、Failureの引数にNonEmptyListを指定する方法です。Failure側とSuccess側の型パラメータを指定しないといけないのでちょっと大変です。

def f(e: Throwable): Validation[NonEmptyList[Throwable], Int] = {
  Failure[NonEmptyList[Throwable], Int](NonEmptyList(e))
}
nel関数

NonEmptyListの生成では、nel関数を用いることができます。NonEmptyListを直接使うより少しだけ短くなります。

def f(e: Throwable): Validation[NonEmptyList[Throwable], Int] = {
  Failure[NonEmptyList[Throwable], Int](nel(e))
}
wrapNelメソッド

任意のオブジェクトをNonEmptyList化するには、wrapNelメソッドを使うことができます。

def f(e: Throwable): Validation[NonEmptyList[Throwable], Int] = {
  Failure[NonEmptyList[Throwable], Int](e.wrapNel)
}
ValidationNEL

「Validation[NonEmptyList[X], Y]」の型は頻繁に用いられるので、この型の別名として「ValidationNEL[X, Y]」という型が定義されています。ValidationNELを使って書き換えると以下のようになります。

def f(e: Throwable): ValidationNEL[Throwable, Int] = {
  Failure[NonEmptyList[Throwable], Int](e.wrapNel)
}
failure関数

failure関数を使ってFailureを生成することができます。手間はFailureを直接使うのとあまり変わりません。

def f(e: Throwable): ValidationNEL[Throwable, Int] = {
  failure[NonEmptyList[Throwable], Int](e.wrapNel)
}
liftFailNelメソッド

Validation[Throwable, Int]がある状態で、これをNonEmptyList版にするには、liftFailNelメソッドを使います。

def f(e: Throwable): ValidationNEL[Throwable, Int] = {
  e.fail[Int].liftFailNel
}
failメソッド

任意のオブジェクトをfailメソッドを用いてFailureにすることができます。Success側の型を型パラメータで指定します。ValidationNELにしたいので、まずオブジェクトをwrapNelメソッドでNonEmptyListにした後に、failメソッドを適用しています。

def f(e: Throwable): ValidationNEL[Throwable, Int] = {
  e.wrapNel.fail[Int]
}
failNelメソッド

failNelメソッドを使えば、任意のオブジェクトをNonEmptyListのFailureにすることができます。

def f(e: Throwable): ValidationNEL[Throwable, Int] = {
  e.failNel[Int]
}

Success

正攻法

Successを生成する正攻法は、Successの引数にNonEmptyListを指定する方法です。こちらもFailure側とSuccess側の型パラメータを指定しないといけないのでちょっと大変です。

def f(a: Int): ValidationNEL[Throwable, Int] = {
  Success[NonEmptyList[Throwable], Int](a)
}
success関数

success関数を使ってSuccessを生成することができます。手間はSuccessを直接使うのとあまり変わりません。

def f(a: Int): ValidationNEL[Throwable, Int] = {
  success[NonEmptyList[Throwable], Int](a)
}
successメソッド

任意のオブジェクトをsuccessメソッドを用いてSuccessにすることができます。Failure側の型を型パラメータで指定します。

def f(a: Int): ValidationNEL[Throwable, Int] = {
  a.success[NonEmptyList[Throwable]]
}
liftFailNelメソッド

Validation[Throwable, Int]がある状態で、これをNonEmptyList版にするには、liftFailNelメソッドを使います。Successの場合も、Failureと同様に使用できます。

def f(a: Int): ValidationNEL[Throwable, Int] = {
  a.success[Throwable].liftFailNel
}
successNelメソッド

failNelメソッドを使えば、任意のオブジェクトをNonEmptyListのSuccessにすることができます。

def f(a: Int): ValidationNEL[Throwable, Int] = {
  a.successNel[Throwable]
}

ノート

Validationを使う場合は、かなりの確率でNonEmptyListとThrowableを併用することになるので「ValidationNEL[Throwable, T]」という形とこの型に対する操作はイディオムとして覚えておくとよいでしょう。

なお、ExceptionではなくThrowableを選んだのは、scala.util.control.Exceptionとの相性を考慮したためです。「Scala Tips / Either (5) - Exception」が参考になると思います。Validation編も整理する予定です。

記事では、ValidationNEL[Throwable, T]を生成する様々な方法を紹介しましたが、普通はfailNelメソッド、successNelメソッドを使うのがよいでしょう。その他の方法は、必要に応じて使い分けることになります。引き出しが多い方が応用が効くので、色々なルートを紹介しました。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

2012年4月17日火曜日

Scala Tips / Validation (2) - 関数で値の取り出し

Validationから値を取り出すイディオムです。

前回は、Validationに格納されている成功(Success)の値を取り出すイディオムでした。失敗(Failure)の場合はデフォルト値を使いました。

今回は、Validationに格納されている成功(Success)と失敗(Failure)を同じ型の値に変換して取り出すイディオムです。

Validation[Throwable, Int]に対して、Successの場合はSuccessの値(Int)をStringに、Failureの場合はFailureの値ThrowableをStringにして返す処理を考えます。SuccessのIntをStringに、FailureのThrowableをStringに変換する関数は外部から与えることにします。

(分類の基準)

Java風

if式でValidation#isSuccessメソッドを使ってSuccessとFailureの判定をして、処理を切り分ける事ができます。SuccessとFailureの値を取り出し、適切な関数を適用します。SuccessとFailureのいずれもasInstanceOfが必要になるので、あまり使いたくない用法です。

def f(a: Validation[Throwable, Int], g: Throwable => String, h: Int => String): String = {
  if (a.isSuccess) {
    h(a.asInstanceOf[Success[Throwable, Int]].a)
  } else {
    g(a.asInstanceOf[Failure[Throwable, Int]].e)
  }
}

Scala風

match式を使うとSuccessとFailureのパターンマッチングで書くことができます。

def f(a: Validation[Throwable, Int], g: Throwable => String, h: Int => String): String = {
  a match {
    case Success(s) => h(s)
    case Failure(e) => g(e)
  }
}

Scala

ValidationはScalazの機能なので、「Scala」というのは変ですが、foldメソッドを用いるのがScala流のコーディングです。Validationのfoldメソッドは、第一引数にFailureの値を変換する関数、第二引数にSuccessの値を変換する関数を指定します。

def f(a: Validation[Throwable, Int], g: Throwable => String, h: Int => String): String = {
  a.fold(g, h)
}

Scalaz

foldメソッドを使う方式は「Scala」のところで紹介しました。その他に、Scalazらしい特徴的な方式はないと思います。

ノート

取り出す値の型がSuccessの値の型が同じ場合には、「|||」メソッドを使うことができます。

Validationの「|||」メソッドは、Successの場合は値の取り出し、Failureの場合は指定したFailureの値を関数で変換して取り出す関数です。

def f(a: Validation[Throwable, Int], g: Throwable => Int): Int = {
  a ||| g
}

また、foldメソッドも第二引数(Successの場合に適用する関数)を省略して、以下のような使い方ができます。

def f(a: Validation[Throwable, Int], g: Throwable => Int): Int = {
  a.fold(g)
}

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

2012年4月16日月曜日

Scala Tips / Validation (1) - 値の取り出し

ValidationはScalazが提供する成功/失敗の計算文脈を提供するモナドです。

Validationを使ってOptionやEitherと同様の成功/失敗の計算文脈上でのMonadicプログラミングをすることができます。ValidationではSuccessで成功、Failureで失敗を表現します。

Validationは、名前から分かるように入力フォームのプロパティやRESTサービスの引数といったところでのバリデーション用途を想定しており、失敗側の情報を積算していけるのが特徴となっています。

今回はValidationから値を取り出すイディオムです。

Validation[Throwable, Int]がSuccessの場合はSuccessの値(Int)を、Failureの場合は0を返す処理を考えます。

(分類の基準)

Java風

if式でValidation#isSuccessメソッドを使ってSuccessとFailureの判定をして、処理を切り分ける事ができます。ただ、asInstanceOfが必要になるので、あまり使いたくない用法です。

def f(a: Validation[Throwable, Int]): Int = {
  if (a.isSuccess) a.asInstanceOf[Success[Throwable, Int]].a else 0
}

Scala風

match式を使うとSuccessとFailureのパターンマッチングで書くことができます。

def f(a: Validation[Throwable, Int]): Int = {
  a match {
    case Success(s) => s
    case Failure(_) => 0
  }
}

Scala

ValidationはScalazの機能なので、「Scala」というのは変ですが、foldメソッドを用いるのがScala流のコーディングです。Validationのfoldメソッドは、第一引数にFailureの値を変換する関数、第二引数にSuccessの値を変換する関数を指定します。

def f(a: Validation[Throwable, Int]): Int = {
  a.fold(_ => 0, identity)
}

第一引数と第二引数はどちらも同じ値を返す関数がデフォルト値になっているので、省略することができます。今回の処理では、Successは格納されている値をそのままとり出せばよいので、第二引数を省略することができます。

def f(a: Validation[Throwable, Int]): Int = {
  a.fold(_ => 0)
}

Scalaz

Validationの「|」メソッドで、Successの場合は値の取り出し、Failureの場合は指定したデフォルト値を取り出すことができます。

def f(a: Validation[Throwable, Int]): Int = {
  a | 0
}

ノート

今回の課題は「|」メソッドがぴったりなので、これを使うのがベストです。思い浮かばなかった場合は、Scalaの常道でfoldを使うのがよいでしょう。

Scalazの他の機能で同じ処理を実現することもできます。こちらは、メリットはないので実際に使用することはないですが、バリエーションとして挙げておきます。

Validationの「|||」メソッドは、Successの場合は値の取り出し、Failureの場合は指定したFailureの値を関数で変換して取り出す関数です。ここでは、引数に必ず0を返す関数を指定して、Failureの場合は0を返すようにしてみました。

def f(a: Validation[Throwable, Int]): Int = {
  a ||| (_ => 0)
}

Validationでの使用方法が思いつかない場合は、よく慣れているOptionに変換して処理を進める方法もあります。

def f(a: Validation[Throwable, Int]): Int = {
  a.toOption | 0
}

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

今回よりScalaのバージョンを2.9.2、Scalazのバージョンを6.0.4に上げました。どちらもメンテナンスバージョンアップなので、基本的には以前の記事の環境と違いはないと思います。

2012年4月13日金曜日

Scala Tips / Either - Index

Eitherが一段落したので記事の一覧表をまとめておきます。

項目内容
Either値の取得
Either (2) - flatMapflatMapで文脈の切替
Either (3) - getOrElsegetOrElseでデフォルト値を指定
Either (4) - getOrElse, FlatMapflatMapで変換後にgetOrElse
Either (5) - Exceptionscala.util.control.Exceptionで例外ハンドリング
Either (6) - Left, RightLeftとRightの生成
Either (7) - forfor式で変換
Either (8) - for, getOrElsefor式で変換後にgetOrElse
Either (9) - 二項演算二項演算の説明
Either (10) - 二項演算,AND「Either:AND」×「値:任意の関数で計算」
Either (11) - ApplicativeEitherに対するApplicative
Either (12) - Applicativeの記述方法Eitherに対するApplicative(続き)
Either (13) - 二項演算,AND,Monoid「Either:AND」×「値:Monoid」
Either (14) - 二項演算,AND,PlusEither:AND」×「値:Plus」
Either (15) - 二項演算,OR「Either:OR」×「値:任意の関数で計算」
Either (16) - 二項演算,OR,Monoid「Either:OR」×「値:Monoid」
Either (17) - 二項演算,OR,Plus「Either:OR」×「値:Plus」
Either (18) - Booleanと相互変換EitherとBooleanの相互変換
Either (19) - Optionと相互変換EitherとOptionの相互変換
Either (20) - Validationと相互変換EitherとValidationの相互変換
Either (21) - BifunctorEitherにBifunctorを適用

以下は以前のScala Tipsの一覧です。

Option

項目内容
Option値の取得
Option (2)nullをOptionに持ち上げる方法
Option (3)map
Option (4)withFilter
Option (5)collect
Option (6)flatMap
Option (7)for式
Option (8)getOrElse
Option (9)withFilter, map, getOrElse
Option (10) - ExceptionflatMap, Exceptionハンドリング
Option (11) - Some/NoneSomeとNoneの記述方法
nullnullを値に持ち上げる方法

Optionについては以下の記事も参考になると思います。

2012年4月12日木曜日

Scala Tips / Either (21) - Bifunctor

EitherをOption的な成功/失敗の文脈の用途での使い方を中心に見てきました。

Eitherの本来の意味は直和であり、プログラミング的には選択(choice)を表現するオブジェクトです。

成功/失敗の文脈では、成功側と失敗側が非対称でしたが、左側と右側を対象に扱うのが本来のEitherらしい使い方と言えます。

本来のEitherらしい使い方として、ScalazのBifunctorを試してみました。

以下では、ちょっと人工的な例ですが:

  • (処理1) Either[Float, Int]をEither[Double, Double]に変換。
  • (処理2) Either[Double, Double]をEither[String, String]に変換。String変換時にEitherがRightの場合は"Right:"、Leftの場合は"Left:"の文字列を文字列の前に連結する。
  • (処理3) Either[String, String]からStringを取り出す。

のシーケンスによる関数を書いてみます。

(分類の基準)

Java風

if式を用いて(処理1)、(処理2)、(処理3)を順に処理します。ifRightの判定とright.getやleft.getの取り出しが泣き別れになってしまうのが、あまりよい感触ではありません。プログラム全体もごちゃっとした感じで可読性も低いです。

def f(a: Either[Float, Int]): String = {
  val b = if (a.isRight) Right(a.right.get.toDouble)
          else Left(a.left.get.toDouble)
  val c = if (b.isRight) Right("Right: " + b.right.get)
          else Left("Left: " + b.left.get)
  if (c.isRight) c.right.get
  else c.left.get
}

Scala風

match式を使うと以下のようになります。こちらの方が綺麗です。ただ、冗長な感じは否めません。

def f(a: Either[Float, Int]): String = {
  val b = a match {
    case Right(x) => Right(x.toDouble)
    case Left(x) => Left(x.toDouble)
  }
  val c = b match {
    case Right(x) => Right("Right: " + x)
    case Left(x) => Left("Left: " + x)
  }
  c match {
    case Right(x) => x
    case Left(x) => x
  }
}

Scala

Scala的にはEitherのfoldメソッドを使って書くのがよい感じです。簡潔で可読性もよくなります。

def f(a: Either[Float, Int]): String = {
  val b = a.fold(x => Left(x.toDouble), x => Right(x.toDouble))
  val c = a.fold(x => Left("Left: " + x), x => Right("Right: " + x))
  c.fold(identity, identity)
}

「c.fold(identity, identity)」のidentityは、引数と同じ値を返す関数で、「x => x」と同等の動きをします。どちらでもよいのですが、ここではidentityを使ってみました。

Scalaz

ScalazではBifunctorという、2つの要素から構成されるオブジェクトに対して、それぞれの要素を処理する関数を同時に記述できる機能を提供しています。EitherはLeftとRightの2つの要素を持っているので、Bifunctorを適用できます。

Bifunctorでは、左側の要素を処理する関数を「<-:」、右側の要素を処理する関数を「:->」で指定します。

今回の課題をBifunctorで記述すると以下のようになります。Bifunctorはモナディックな動きをするので、EitherからEitherへの変換になります。Eitherから最後に値を取り出す処理はfoldメソッドを用います。

def f(a: Either[Float, Int]): String = {
  val b = ((_: Float).toDouble) <-: a :-> ((_: Int).toDouble)
  val c = ("Left: " + _) <-: b :-> ("Right: " + _)
  c.fold(identity, identity)
}

プログラムの動きが分かりやすいように、途中結果を格納する変数に型を明記すると以下のようになります。

def f(a: Either[Float, Int]): String = {
  val b: Either[Double, Double] =
    ((_: Float).toDouble) <-: a :-> ((_: Int).toDouble)
  val c: Either[String, String] =
    ("Left: " + _) <-: b :-> ("Right: " + _)
  c.fold(identity, identity)
}

Bifunctorは以下のように連結することができます。このようにすると、左側右側の動きが分かりやすくなります。

def f(a: Either[Float, Int]): String = {
  ("Left: " + _) <-: ((_: Float).toDouble) <-: a :-> ((_: Int).toDouble) :-> ("Right: " + _) fold(identity, identity)
}

ただ、左右を連結していくと長くなってしまうのが難点です。以下のように各段の処理を関数化して、これをつなげていくと全体の流れが見えやすくなります。

def f(a: Either[Float, Int]): String = {
  def lds = "Left: " + (_: Double)
  def lfd = (_: Float).toDouble
  def rds = "Right: " + (_: Double)
  def rfd = (_: Int).toDouble
  val b = lds <-: lfd <-: a :-> rfd :-> rds
  b.fold(identity, identity)
}

ノート

Bifunctorは面白い機能で、Eitherでも便利に使えそうですが、Eitherに関しては「Scala」で使用したfoldメソッドを使う方法が処理も簡潔で意図も明確なのでよさそうです。

Bifunctorの場合は、Either以外にTuple2やValidation、さらにjava.util.Map.Entryも対象となっています。この他にも、型クラスインスタンスを定義すれば2つの要素を持つ任意のオブジェクトを処理対象にすることが可能です。

つまり、2つの要素を持つオブジェクトに対する共通処理を記述する場合にはBifunctorを用いる意味が出てきます。

たとえば、今回の例を拡張して以下のような関数を定義します。型パラメータを用いて、Bifunctorのオブジェクトを処理対象に指定しています。(Bifunctorには値を取り出す機能がないので、Either#foldメソッドで行っていた値を取り出す処理は省いています。)

def f[M[_, _]: Bifunctor](a: M[Float, Int]): M[String, String] = {
  def lds = "Left: " + (_: Double)
  def lfd = (_: Float).toDouble
  def rds = "Right: " + (_: Double)
  def rfd = (_: Int).toDouble
  lds <-: lfd <-: a :-> rfd :-> rds
}

EitherのRightとLeftに適用すると、以下のようになります。

scala> f(1.right[Float])
res16: Either[String,String] = Right(Right: 1.0)

scala> f(1.0F.left[Int])
res18: Either[String,String] = Left(Left: 1.0)

Tupleに適用すると、以下のようになります。

scala> f((1.0F, 1))
res19: (String, String) = (Left: 1.0,Right: 1.0)

ScalaのMapは、Tupleに対するIteratorでもあるので、以下のようにmapメソッドを用いて適用することができます。

scala> Map(1.0F -> 1, 2.0F -> 2).map(f(_))
res34: scala.collection.immutable.Map[String,String] = Map(Left: 1.0 -> Right: 1.0, Left: 2.0 -> Right: 2.0)

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

2012年4月11日水曜日

Scala Tips / Either (20) - Validationと相互変換

EitherとValidationの相互変換のイディオムです。

scalaz.ValidationはScalazが提供する「検証の文脈」のモナドです。「成功と失敗の文脈」としても使うことができるので、OptionやEitherと適材適所で使い分けしていくことになります。

Either編の後はValidation編に入る予定ですが、前回「Optionと相互変換」でも「o.toSuccess(e).either」という形で登場したので、少し内容を先取りして取り上げます。

(分類の基準)

Java風

Validation→Either

ValidationをEitherに変換する場合は、if式による判定を用いて、Validationの成否に対応する値をLeftまたはRightに詰めます。

Validationでは、成功または失敗の値を直接取り出す方法がないので、キャストを用いています。あまり美しくないですね。

def f[T](v: Validation[Throwable, T]): Either[Throwable, T] = {
  if (v.isSuccess) Right(v.asInstanceOf[Success[Throwable, T]].a)
  else Left(v.asInstanceOf[Failure[Throwable, T]].e)
}

キャストはちょっと筋が悪いので、美しさを追求する場合は、一度Optionに変換する方法もあります。

def f[T](v: Validation[Throwable, T]): Either[Throwable, T] = {
  if (v.isSuccess) Right(v.toOption.get)
  else Left(v.fail.toOption.get)
}
Either→Validation

EitherをValidationに変換する場合は、EitherのisRihtメソッドを用いて判定し、rightメソッドで取り出したRightProjectionまたはleftメソッドで取り出したLeftProjectionからgetメソッドで値を取り出し、SuccessまたはFailureに詰めます。

def f[T](e: Either[Throwable, T]): Validation[Throwable, T] = {
  if (e.isRight) Success(e.right.get)
  else Failure(e.left.get)
}

Scala風

Validation→Either

ValidationをEitherに変換する場合は、match式でSuccessまたはFailureへの判定を用いて、Validationの成否に対応する値をLeftまたはRightに詰めます。

def f[T](v: Validation[Throwable, T]): Either[Throwable, T] = {
  v match {
    case Success(s) => Right(s)
    case Failure(f) => Left(f)
  }
}
Either→Validation

EitherをValidationに変換する場合は、match式でLeftまたはRightへの判定を用いて、Eitherの成否に対応する値をSuccessまたはFailureに詰めます。

def f[T](e: Either[Throwable, T]): Validation[Throwable, T] = {
  e match {
    case Right(r) => Success(r)
    case Left(l) => Failure(l)
  }
}

Scala

ValidationはScalazが提供するモナドですが、ここではScala的なコーディングということでfoldメソッドを用いた方法を紹介します。

Validation→Either

Validationのfoldメソッドの第1引数に失敗側の値をLeftに変換する関数を、第2引数に成功側の値をRightに変換する関数を指定します。

def f[T](v: Validation[Throwable, T]): Either[Throwable, T] = {
  v.fold(Left(_), Right(_))
}

Scalazの機能を用いて、よりコンパクトに書くと以下のようになります。

def f[T](v: Validation[Throwable, T]): Either[Throwable, T] = {
  v.fold(_.left, _.right)
}
Either→Validation

Eitherのfoldメソッドの第1引数に左側の値をFailureに変換する関数を、第2引数に右側の値をSuccessに変換する関数を指定します。

def f[T](e: Either[Throwable, T]): Validation[Throwable, T] = {
  e.fold(Failure(_), Success(_))
}

Scalazの機能を用いて、よりコンパクトに書くと以下のようになります。

def f[T](e: Either[Throwable, T]): Validation[Throwable, T] = {
  e.fold(_.fail, _.success)
}

Scalaz

Validation→Either

Validationには、Eitherに変換するためのeitherメソッドが用意されているので、これを用います。

def f[T](v: Validation[Throwable, T]): Either[Throwable, T] = {
  v.either
}
Either→Validation

Scalazでは、validation関数でEitherをValidationに変換することができます。

def f[T](e: Either[Throwable, T]): Validation[Throwable, T] = {
  validation(e)
}
追記:2012-04-16

Either→ValidationのScalaz版の内容が間違っていたので書き換えました。

ノート

ValidationとEitherの相互変換は、ダイレクトに変換するメソッドが提供されているので、これを用いればよいのですが、Scalaでのプログラミングの考え方をおさらいするのによい例題なのでJava風、Scala風、Scalaの項目も考えてみました。

何かの処理をScalaでプログラミングする場合、以下の順番でロジックを探す事になると思います。

  1. Scalaz/Scalaでドンピシャのメソッドを見つける
  2. fold系やmap, flatMapといった高階関数を使ってロジックを考える
  3. match式を使って場合分けをする
  4. if式を使って場合分けをする

この順番は、Scala Tipsで用いている分類の基準であるScalaz、Scala、Scala風、Java風とも近しいものになっています。

個人的には(パターンマッチングや再帰呼び出し、代数的データ型の選択といったところではなく)成否判定にmatch式が出てきたら負けっぽい感じで考えています。if式はさらに負け度が大きい感じです。この場合、できるだけ2の「fold系やmap, flatMap」に持ち込むように考えていきます。

なぜ、「fold系やmap, flatMap」の方が好ましいのかというと、感覚的には、手続きではなく宣言に近づいていくから、ということになります。具体的なメリットとしては、(1)ロジックを簡潔に記述することができる、(2)部品(関数)の再利用の可能性が高まる、ということです。

OOPでは「fold系やmap, flatMap」という切り口のプログラミングはあまりないので、OOPプログラマがScala(関数型言語)を使う上で意識しておくべきコツと言うことができそうです。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

2012年4月10日火曜日

Scala Tips / Either (19) - Optionと相互変換

EitherとOptionの相互変換のイディオムです。

EitherのRightを成功、Leftを失敗として扱うことを想定しています。逆の場合は、成否の判定を反転させてください。

(分類の基準)

Java風

Option→Either

OptionをEitherに変換する場合は、OptionのifDefinedメソッドでSome(成功の文脈)の判定を行い、Someの場合はその値をRightに、そうでない場合は失敗の内容を示す値(この場合はThrowable)をLeftに詰めます。

def f[T](o: Option[T], e: Throwable): Either[Throwable, T] = {
  if (o.isDefined) Right(o.get) else Left(e)
}

Optionでは、成功と失敗の切り分けはできますが、失敗の内容を示す値は保持していないので、外側から指定する必要があります。

Either→Option

EitherをOptionに変換する場合は、EitherのisRihtメソッドで成功(Right)であることを確認した後、rightメソッドで取得したRightProjectionのrightメソッドを用いて値を取り出し、Someに詰めます。EitherがLeftの場合は、Left側の値は捨ててNoneを返します。

def f[T](e: Either[Throwable, T]): Option[T] = {
  if (e.isRight) Some(e.right.get)
  else None
}

Scala風

Option→Either

OptionをEitherに変換する場合は、match式を用いて、SomeとNoneの判定を行い、Someの場合は格納されている値をRightに詰めます。Noneの場合は、失敗を示す値をLeftに詰めます。

def f[T](o: Option[T], e: Throwable): Either[Throwable, T] = {
  o match {
    case Some(s) => Right(s)
    case None => Left(e)
  }
}
Either→Option

EitherをOptionに変換する場合は、match式でLeftまたはRightを判定し、Rightの場合は取得した値をSomeに詰めます。Leftの場合は、Left側の値は捨ててNoneを返します。

def f[T](e: Either[Throwable, T]): Option[T] = {
  e match {
    case Right(s) => Some(s)
    case Left(_) => None
  }
}

Scala

Option→Either

OptionをEitherに変換するMonadicな演算はないと思います。Scala風で説明したmatch式方式を用いるとよいでしょう。

Either→Option

EitherをOptionに変換する場合、Eitherのfoldメソッドを使うことができます。

def f[T](e: Either[Throwable, T]): Option[T] = {
  e.fold(_ => None, Some(_))
}

Scalaz

Option→Either

Scalazでは、OptionをValidationに変換するtoSuccessメソッドが追加されています。ValidationはさらにEitherに変換するeitherメソッドを持っているので、これを連結して使用します。

def f[T](o: Option[T], e: Throwable): Either[Throwable, T] = {
  o.toSuccess(e).either
}
Either→Option

ScalazではEitherをOptionに変換する便利なメソッドは用意していません。Javaで説明したfoldメソッドを用いるとよいでしょう。

ノート

EitherとOptionの相互変換は以下の手法がよいでしょう。

Either→Option
Eitherのfoldメソッド (Scala)
Option→Either
Validation経由 (Scalaz)

性能を重視する場合はJava風、ScalaやScalazの技が思い浮かばなかった場合はScala風のmatch式が安全策です。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

2012年4月9日月曜日

Scala Tips / Either (18) - Booleanと相互変換

EitherとBooleanの相互変換のイディオムです。

EitherのRightを真、Leftを偽として扱うことを想定しています。逆の場合は、真偽の判定を反転させてください。

(分類の基準)

Java風

Boolean→Either

BooleanをEitherに変換する場合は、if式による判定を用いて、Booleanの真偽に対応する値をLeftまたはRightに詰めます。

def f[T](v: Boolean, e: Throwable, s: T): Either[Throwable, T] = {
  if (v) Right(s) else Left(e)
}
Either→Boolean

EitherをBooleanに変換する場合は、EitherのisRihtメソッドを用います。

def f[T](e: Either[Throwable, T]): Boolean = {
  e.isRight
}

Scala風

Boolan→Either

BooleanをEitherに変換する場合は、match式による判定を用いて、Booleanの真偽に対応する値をLeftまたはRightに詰めます。

def f[T](v: Boolean, e: Throwable, s: T): Either[Throwable, T] = {
  v match {
    case true => Right(s)
    case false => Left(e)
  }
}

しかし、Java風で説明したif式方式の方が簡明なのでそちらを用いたほうがよいでしょう。

Either→Boolean

EitherをBooleanに変換する場合は、match式でLeftまたはRightで判定することができます。

def f[T](e: Either[Throwable, T]): Boolean = {
  e match {
    case Right(_) => true
    case Left(_) => false
  }
}

しかし、Java風で説明したifRightメソッドの方が簡明なのでそちらを用いたほうがよいでしょう。

Scala

Boolan→Either

BooleanをEitherに変換するMonadicな演算はないと思います。Java風で説明したif式方式を用いるとよいでしょう。

Either→Boolean

EitherをBooleanに変換するMonadicな演算はないと思います。Java風で説明したifRightメソッドを用いるとよいでしょう。

Scalaz

Boolan→Either

Scalazでは、Booleanにeitherメソッドが追加されており、以下のようにしてEitherを生成することができます。eitherの直後にLeft側の値、orの直後にRight側の値を指定します。

def f[T](v: Boolean, e: Throwable, s: T): Either[Throwable, T] = {
  !v either e or s
}

直感的にはeitherの直後に真の値、orの直後に偽の値が入りますが、実際にはeitherの直後はLeftなので偽の値、orの直後はRightなので真の値が入ります。Boolean値の判定を反転させることで、意図した動作になります。

Either→Boolean

ScalazではEitherをBooleanに変換する便利なメソッドは用意していません。Java風で説明したifRightメソッドを用いるとよいでしょう。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

2012年4月6日金曜日

Scala Tips / Either (17) - 二項演算, OR, Plus

Rightを成功とする、成功/失敗文脈におけるEitherに対する二項演算です。(Scala Tips / Either (9) - 二項演算)

ここまで「Either:AND」について「Either:AND」×「値:任意の関数で計算」「Either:AND」×「値:Monoid」「Either:AND」×「値:Plus」をみてきました。

また「Either:OR」について「Either:OR」×「値:任意の関数で計算」「Either:OR」×「値:Monoid」を見てきました。

今回は、値に対する二項演算としてPlusの<+>を考えます。Plusは「Scala Tips / Either (14) - 二項演算, AND, Plus」で説明したとおりコンテナの連結を行う型クラスと思われます。

値に対する二項演算は以下の組合せとします。

lhs/rhsともRight
Plusの<+>で計算
lhs/rhsともLeft
lhs側を使う

(分類の基準)

Java風

if式を使って、4つの場合を記述します。「Rightの二項演算」の所で型クラスPlusの演算子<+>を用いて型クラスPlusの型クラスインスタンスを持つオブジェクト、つまり連結機能を持つコンテナオブジェクトの連結を行っています。

def f[M[_]: Plus, A](e1: Either[Throwable, M[A]], e2: Either[Throwable, M[A]]): Either[Throwable, M[A]] = {
  if (e1.isRight && e2.isRight) {
    Right(e1.right.get <+> e2.right.get) // Rightの二項計算
  } else if (e1.isRight && e2.isLeft) {
    e1
  } else if (e1.isLeft && e2.isRight) {
    e2
  } else { // e1.is Left && e2.isLeft
    e1 // Leftの二項演算
  }
}

Scala風

match式を使って、4つの場合を記述します。「Rightの二項演算」の所で型クラスPlusの演算子<+>を用いてコンテナオブジェクトの連結を行っています。

def f[M[_]: Plus, A](e1: Either[Throwable, M[A]], e2: Either[Throwable, M[A]]): Either[Throwable, M[A]] = {
  e1 match {
    case Right(e1r) => e2 match {
      case Right(e2r) => Right(e1r <+> e2r) // Rightの二項計算
      case Left(_) => e1
    }
    case Left(_) => e2 match {
      case Right(_) => e2
      case Left(_) => e1 // Leftの二項演算
    }
  }
}

match式のネストが気に入らない場合は以下のようにすればネストしない方式で記述することもできます。

def f[M[_]: Plus, A](e1: Either[Throwable, M[A]], e2: Either[Throwable, M[A]]): Either[Throwable, M[A]] = {
  (e1, e2) match {
    case (Right(e1r), Right(e2r)) => Right(e1r <+> e2r) // Rightの二項計算
    case (Right(_), Left(_)) => e1
    case (Left(_), Right(_)) => e2
    case (Left(_), Left(_)) => e1 // Leftの二項計算
  }
}

後者(Tuple方式)は、Tupleを導入しているのとパターンマッチングの回数が増えるので性能的には不利ですが、プログラムの見通しはよくなります。フレームワークで使う場合には性能重視で前者(ネスト方式)、アプリケーションで使う場合には可読性重視で後者(Tuple方式)という選択も考えられます。

Scala

Eitherに対するORを行うScalaらしい関数合成、Monadic演算による方式を見つけることができませんでした。Scala風で説明した方法を用いることになります。(「Scala Tips / Either (15) - 二項演算, OR」と同じです。)

Scalaz

Scalazでも、Eitherに対するORを行うScalaらしい関数合成、Monadic演算による方式を見つけることができませんでした。Scala風で説明した方法を用いることになります。

(「Scala Tips / Either (15) - 二項演算, OR」と同じです。)

ノート

型クラスPlusについては、型クラスMonoidとの違い、コンテキスト・バウンドでの指定方法という話題があります。詳しくは「Scala Tips / Either (14) - 二項演算, AND, Plus」のノートを参照してください。

演算仕様

EitherのOR

EitherのORは以下の演算になります。

EitherのOR
lhsrhs結果Rightの値Leftの値
RightRightRight二項演算-
RightLeftRightlhs-
LeftRightRightrhs-
LeftLeftLeft-二項演算

lhsとrhsの両方がLeft(失敗)でない場合は、Right(成功)となります。

値に対する二項演算

値に対する二項演算は、lhs/rhsともRightだった場合と、Leftだった場合があります。

値に対する二項演算は、以下のものが考えられます。

lhs
lhs側を使う
rhs
rhs側を使う
  • f(lhs, rhs) :: 任意の関数で計算
  • lhs |+| rhs :: Monoidで計算
  • lhs <+> rhs :: Plusで計算

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

2012年4月5日木曜日

Scala Tips / Either (16) - 二項演算, OR, Monoid

3月2日以来ですが、OFADが一段落したのでScala Tipsを再開します。

Rightを成功とする、成功/失敗文脈におけるEitherに対する二項演算です。(Scala Tips / Either (9) - 二項演算)

ここまで「Either:AND」について「Either:AND」×「値:任意の関数で計算」「Either:AND」×「値:Monoid」「Either:AND」×「値:Plus」をみてきました。

前回(3月2日)「Scala Tips / Either (15) - 二項演算, OR」は「Either:OR」×「値:任意の関数で計算」を考えました。

今回は、値に対する二項演算としてMonoidの|+|を考えます。

値に対する二項演算は以下の組合せとします。

lhs/rhsともRight
Monoidの|+|
lhs/rhsともLeft
lhs側を使う

(分類の基準)

Java風

if式を使って、4つの場合を記述します。コンテキスト・バウンドでMonoid型の型パラメータTを定義して、Eitherの右側で使っています。

def f[T: Monoid](e1: Either[Throwable, T], e2: Either[Throwable, T]): Either[Throwable, T] = {
  if (e1.isRight && e2.isRight) {
    Right(e1.right.get |+| e2.right.get) // Rightの二項計算
  } else if (e1.isRight && e2.isLeft) {
    e1
  } else if (e1.isLeft && e2.isRight) {
    e2
  } else { // e1.is Left && e2.isLeft
    e1 // Leftの二項演算
  }
}

2つのEitherの両方が右側だった時の動作は:

  • Right(e1.right.get |+| e2.right.get)

となります。「|+|」がMonoidの演算です。一般的に加法的な演算が行われます。

Scala風

match式を使って、4つの場合を記述します。

def f[T: Monoid](e1: Either[Throwable, T], e2: Either[Throwable, T]): Either[Throwable, T] = {
  e1 match {
    case Right(e1r) => e2 match {
      case Right(e2r) => Right(e1r |+| e2r) // Rightの二項計算
      case Left(_) => e1
    }
    case Left(_) => e2 match {
      case Right(_) => e2
      case Left(_) => e1 // Leftの二項演算
    }
  }
}

match式のネストが気に入らない場合は以下のようにすればネストしない方式で記述することもできます。

def f[T: Monoid](e1: Either[Throwable, T], e2: Either[Throwable, T]): Either[Throwable, T] = {
  (e1, e2) match {
    case (Right(e1r), Right(e2r)) => Right(e1r |+| e2r) // Rightの二項計算
    case (Right(_), Left(_)) => e1
    case (Left(_), Right(_)) => e2
    case (Left(_), Left(_)) => e1 // Leftの二項計算
  }
}

後者(Tuple方式)は、Tupleを導入しているのとパターンマッチングの回数が増えるので性能的には不利ですが、プログラムの見通しはよくなります。フレームワークで使う場合には性能重視で前者(ネスト方式)、アプリケーションで使う場合には可読性重視で後者(Tuple方式)という選択も考えられます。

Scala

Eitherに対するORを行うScalaらしい関数合成、Monadic演算による方式を見つけることができませんでした。Scala風で説明した方法を用いることになります。(「Scala Tips / Either (15) - 二項演算, OR」と同じです。)

Scalaz

Scalazでも、Eitherに対するORを行うScalaらしい関数合成、Monadic演算による方式を見つけることができませんでした。Scala風で説明した方法を用いることになります。

(「Scala Tips / Either (15) - 二項演算, OR」と同じです。)

ノート

Scala Tips / Either (13) - 二項演算, AND, Monoid」では、Java風、Scala風、Scalaの実装はJavaやScala標準にはMonoidがないためIntを使用していました。

Scala Tips / Either (15) - 二項演算, OR」で、検討した時からEitherに対するORをスマートに行う方法は進展がなく、Java風、Scala風のプログラムのみ取り上げます。そこで今回はJava風、Scala風のコーディングにScalazを併用してみました。

演算仕様

EitherのOR

EitherのORは以下の演算になります。

EitherのOR
lhsrhs結果Rightの値Leftの値
RightRightRight二項演算-
RightLeftRightlhs-
LeftRightRightrhs-
LeftLeftLeft-二項演算

lhsとrhsの両方がLeft(失敗)でない場合は、Right(成功)となります。

値に対する二項演算

値に対する二項演算は、lhs/rhsともRightだった場合と、Leftだった場合があります。

値に対する二項演算は、以下のものが考えられます。

lhs
lhs側を使う
rhs
rhs側を使う
  • f(lhs, rhs) :: 任意の関数で計算
  • lhs |+| rhs :: Monoidで計算
  • lhs <+> rhs :: Plusで計算

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

2012年4月4日水曜日

Object-Functional Analysis and Designまとめのまとめ

3月19日(月)に要求開発アライアンスのセッション『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』を行いました。その事前準備、まとめ、回顧の記事が一段落したのでリンクをまとめました。

今回のセッションはちょうど、クラウド・コンピューティングや関数型言語への理解が進んで、開発方法論やアプリケーション・アーキテクチャについて腰を落として考えるちょうどよいタイミングだったので、時間を取って色々な要素技術について改めて考えてみました。

今回50分のセッションで、それらの要素をすべて盛り込むことはできないので、事前資料や回顧の記事を書くことで、スライドを書きながら考えていたことを成果物としてまとめてみました。実際に、文章にまとめてみると自分的にも色々な発見もありますし、フィードバックから新たな情報も得られるので、非常に有益だったと思います。

まとめのまとめ

スライドとブログ記事を通じてひと通り考えてきたことをまとめると以下のようになります。

クラウド・アプリケーション・アーキテクチャ
CQRS/EDAが有力。EIP、DDDの技術も適用できる。
モデリング
引き続きOOモデリングが中心。CQRS/EDAをターゲットにイベントを中心としたモデリングが有効。
関数型モデリング
EDAのイベントコンシュマーでデータフローを記述する部分でOOモデリングを補完。OFPとDSLで実装とつなげる。
DSL
DSL指向フレームワークによるDSL指向プログラミングに移行。
OFP
DSL、並行プログラミングのニーズからクラウド・アプリケーションの主力言語になる。

個人的にはEDAの重要性を再認識したのが収穫で、EDAとOOAD、OFP、DSL、DDD、DCI、EIPといった技術との関係も整理することができました。

その上で、今後の大きな流れを考えてみると:

  • クラウド・アプリケーションはOFPとDSLで記述するようになる
  • EDAが新しい枠組みの上で再構築/再発明されていく

という事になろうかと思います。

スライド

当日使用したスライドは以下のものです。(PDF出力ツールの関係で、当日は非表示にしたスライドも表示されています)

まとめ

セッション後のまとめは以下の記事になります。

2012年4月3日火曜日

関数型とデーターフロー(5)

3月19日(月)に要求開発アライアンスのセッション『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』を行いましたが、説明を端折ったところを中心にスライドの回顧をしています。

今回は「KliesliでOption/Listを合成」として用意した以下のスライドを説明します。

関数型でデータフローを記述する方式を以下の事前準備の記事で考えました。

関数型とデータフロー(4)」では、右の図を作成しました。Kleisliを用いてOptionモナドを合成しています。

この図をスライドに収めるために簡略化したものが右の図です。

スライド作成時にListモナドの合成も入れておいた方がよいと考え、右の図も作成し、スライドには左右に併置する形で入れました。

この新しく追加した図「KleisliによるListモナドの合成」について「関数型とデータフロー(4)」にならってについて説明します。

Listモナドの合成

ScalazはKleisliのための一連の機能を提供しています。☆はモナドのbind演算に用いる関数をKleisliに入れる演算を行う関数です。

val plus5l = (a: List[Int]) => List(a.map(_ + 5))
val mul10l = (a: List[Int]) => List(a.map(_ * 10))
val plus5lk = ☆(plus5l)
val mul10lk = ☆(mul10l)

☆はkleisli関数の別名なので、以下のように書くこともできます。

val plus5lk = kleisli(plus5l)
val mul10lk = kleisli(mul10l)

☆(plus5l)によってKleisli化されたplus5lが、☆(mul10l)によってKleisli化されたmul10lが定義されます。それぞれにplus5lk、mul10lkという名前をつけています。

関数plus5lkとmul10lkはそれぞれ以下のように動作します。Int型を引数にとってOption[Int]型を返します。

scala> List(1, 2, 3) |> plus5l
res16: List[List[Int]] = List(List(6, 7, 8))

scala> List(1, 2, 3) |> mul10l
res25: List[List[Int]] = List(List(10, 20, 30))

plus5lkとmul10lkは、いずれもKleisli化されている(Kleisliの容器に入っている)ので、以下のように演算子>=>で合成することができます。合成して作成した新しいモナド演算を行う関数にp5m10okという名前をつけます。

scala> val p5m10lk = plus5lk >=> mul10lk
p5m10lk: scalaz.Kleisli[List,List[Int],List[Int]] = scalaz.Kleislis$$anon$1@2d688eb5

以下のように期待通りに動作しました。

scala> List(1, 2, 3) |> p5m10lk
res26: List[List[Int]] = List(List(60, 70, 80))

ノート

データフローの実現方式にListモナドを入れたのは、Listモナドを使った並列プログラミングへ言及したかったためで、セッションでは口頭でその点に触れることができました。

ただ、今見返してみるとこの図の範囲だと、Optionを使って以下のようにした方が自然そうです。

val plus5l = (a: List[Int]) => a.map(_ + 5).some
val mul10l = (a: List[Int]) => a.map(_ * 10).some
val plus5lk = ☆(plus5l)
val mul10lk = ☆(mul10l)
val p5m10lk = plus5lk >=> mul10lk

実行結果は以下のようになります。

scala> List(1, 2, 3) |> p5m10lk
res36: Option[List[Int]] = Some(List(60, 70, 80))

List処理を並列化する場合は、以下のようにパラレルコレクションを使いますが、この場合Kleisliの合成に使うモナドはListモナドではなくOptionモナドでもよいわけです。

scala> val plus5l = (a: List[Int]) => a.par.map(_ + 5).some
plus5l: List[Int] => Option[scala.collection.parallel.immutable.ParSeq[Int]] = <function1>
Promise

ここで、ListモナドやOptionモナドではなく、並列処理を専門に扱うモナドを導入するとさらに高度な並行プログラミングを行うことができます。そのような目的でScalazがサポートしているのがPromiseです。

Promiseについては以下のスライドが参考になります。

たとえば、以下のようにOptionモナドをKleisli化していたところを:

val plus5lk = ☆((a: List[Int]) => a.map(_ + 5).some)
plus5lk: scalaz.Kleisli[Option,List[Int],List[Int]] = scalaz.Kleislis$$anon$1@695a1b07

以下のようにすると:

scala> val plus5lk = ((a: List[Int]) => a.map(_ + 5)).promise
plus5lk: scalaz.Kleisli[scalaz.concurrent.Promise,List[Int],List[Int]] = scalaz.Kleislis$$anon$1@6f17b190

PromiseモナドをKleisli化されたものが返ってきます。(上側の型「scalaz.Kleisli[Option,List[Int],List[Int]]」と下側の型「scalaz.Kleisli[scalaz.concurrent.Promise,List[Int],List[Int]]」を比較するとイメージが湧いてくると思います。)このような形に持ってくると、Promiseモナドを使った高度な並行プログラミングが可能になるわけです。

関数やモナドの合成で、安全に高度な並行プログラミングを行うことが、今後のScalaプログラミングの方向性のひとつになってくるはずです。また、今回のセッションの中心的な話題であったデータフローモデルの実現手法という意味でも重要な技術です。

このあたりはKleisliやPromise以外にも色々な技がありそうなので、本ブログでも整理をしていきたいと思っています。

諸元

当日使用したスライドは以下のものです。(PDF出力ツールの関係で、当日は非表示にしたスライドも表示されています)

このスライド作成の様子は以下の記事になります。

まとめは以下の記事になります。

回顧は以下の記事になります。

2012年4月2日月曜日

CQRS, EDA

3月19日(月)に要求開発アライアンスのセッション『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』を行いましたが、説明を端折ったところを中心にスライドの回顧をしています。

セッションでは、オブジェクト関数型言語が中心的な実装言語となった時のOOADの進化の方向性、という観点でストーリ展開してきました。ここでは、クラウド・プラットフォームは、このストーリを考える上での文脈の一つとして捉えています。

今回は逆に、クラウド・アプリケーション・アーキテクチャを軸にしてセッションの情報を再構成してみたいと思います。

クラウド・アプリケーションのイメージ

今回のセッションでは説明しなかったのですが、従来のアプリケーションとクラウド・アプリケーションはそれぞれこのようにイメージしています。

基本となるのは画面とデータベース間のOLTPの枠組みでの転記で、帳票としての印刷も重要なモジュールです。外部連携や通信は特別扱いの連携モジュールを用いて行います。OLTPで賄えない部分は、夜間などの定刻に起動するバッチ処理で行います。ここが非同期的に動作する部分です。

このあたりの基本構造はメインフレームからオフコン、クラサバを経てWebアプリケーションまで変わっていません。基本的には、画面からのリクエストでジョブがキックされてトランザクション配下で同期型に転記処理が遂行されます。プログラムの作り方はずいぶん変わりましたが、アーキテクチャの根幹のところは案外共通しているわけです。

一方、クラウド・アプリケーションの場合、クラウド内で生起する様々な事象がシグナルとしてクラウド・アプリケーションに上がってくるようになります。このシグナルを自アプリケーションにとって意味のあるイベントであると認識するフィルタ処理から始まってイベント駆動でアプリケーションが動作します。イベントの発生は、クラウド上に遍在する複数のイベント発生源上で同時に起きるので、非同期、並行で処理を遂行していく必要があります。

EDA

実際のところ、エンタープライズアプリケーションも前述のレガシーアプリケーションのままでは時代のニーズに合わないので、地道に進化してきており、SOAの傘の下でさまざまな技術が発展しています。しかしSOAというと、合併した企業間のシステム統合や、業務改革と連動した巨大エンタープライズ・アプリケーションの再構築といった、雲の上の話であることが多く、そういった案件以外での認知度は今ひとつです。

しかし、この中で蓄積されてきた技術は、クラウド・アプリケーションの基盤技術として有効であるというのがボクの認識です。ただし、実装技術は古いので、新しい革袋の中で再構築していく必要があるでしょう。その新しい革袋はRESTであったり、関数型であったり、DSLであったりするはずです。

そういった、イベント駆動のアプリケーション向けのアプリケーション・アーキテクチャとしてEDA(Event-Driven Architecture)が知られています。イベント駆動アプリケーションと関数型による実現は「EDAとオブジェクトと関数」や「DCI (Data Context Interaction)」で考察しました。

EIP

EIP(Enterprise Integration Patterns)もクラウド・アプリケーションに転用できる重要な技術です。

EDAを実現する基盤メカニズムとして、ESB(Enterprise Service Bus)やMOM(Message-Oriented Middleware)があり、これを使用するアプリケーション構築パターンとしてEIPが整備されています。

今回は、Apache CamelによるScala DSLとして右の図を用意しました。この図はデータフローに対する関数型&DSLの記述力の例として用意しましたが、クラウド・アプリケーション・アーキテクチャ上に対する重要なアプローチと考えられるEIPの紹介という意味もあります。

EIPは元々企業アプリケーション・アーキテクチャをインテグレーションするためのパターンで、対象の粒度が企業アプリケーションですから、相当大きなものになります。しかし、このパターンはコンポーネントといった、もっと小さな粒度にも適用できると思われます。Apache CamelによるScala DSLもこういった路線が狙いと思います。

今回のセッションでは関数型&DSLを用いてデータフローを記述するアプローチで、関数型とクラウド・アプリケーションの連携を考えています。EIPも従来型のESBのXML定義ファイルベースではなく、関数型&DSLの枠組みでクラウド・アプリケーションに組み込まれていくことになるでしょう。

モデリング

アプリケーション・アーキテクチャが、EDAベースになっていくとすると、モデリングの軸となるモデル要素としてイベントが非常に重要になってきます。

元々、オブジェクト・モデリングではイベントが重要なモデル要素だったので、この点を素直に伸ばしていけば、クラウド・アプリケーションにも適用できるはずです。

この点を盛り込んだ「メタモデル」をセッションで紹介しました。

また、「EDAとオブジェクトと関数」ではイベントをEDA上で実現するメカニズムについても考えました。

CQRSとEDA

CQRS(Command Query Responsibility Segregation)はクラウド・アプリケーション向けに注目されているアプリケーション・アーキテクチャです。

この記事の先頭に挙げたスライド「CQRS, EDA」はCQRSでのCommandで、イベントを発生させ、イベントを起点にEDA的なメカニズムで処理を実行していく事を説明する目的のものです。セッションでは時間の関係で省略しました。

コマンドの発行をイベントの発行と連動させ、EDA上で実現するとEDAの非同期的な実行がCQRSの意図する振舞いとぴったりはまります。CQRS&EDAの組合せが全体のバランスがよいのではないかという提案が、このスライドの意図です。

イベントは、モデリング上で抽出された"ビジネス・イベント"を中心に考え、実装の必要に応じてシステム・イベントを併用する形になるでしょう。このようにすることで、前述のメタモデルをベースとしたオブジェクト・モデリングともつながってきます。

今回のセッションでの考察では、このアーキテクチャの中で、関数型はイベント発生に対応するイベントコンシュマーの実現で用いられることになります。短いものは関数型言語で直接記述して同期実行、大きなものはSparkのようなDSLで記述して非同期実行という使い分けになるでしょう。

追記
イベントコンシュマーをイベントプロデューサに誤記していたので訂正しました。

諸元

当日使用したスライドは以下のものです。(PDF出力ツールの関係で、当日は非表示にしたスライドも表示されています)

このスライド作成の様子は以下の記事になります。

まとめは以下の記事です。

回顧は以下の記事になります。