2012年2月29日水曜日

関数型言語の技術マップ

要求開発アライアンス定例会で『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』というタイトルでセッションを行うことになりました。

セッション時間が50分なので、かなり俯瞰した形での全体像の説明になりそうですが、関連する要素技術の数が多いのと、内容が込み入っているので、ブログで補足説明をすることにしました。

今回はその第一弾です。

「関数型言語の関連技術」として用意した以下の図を説明します。関数型プログラミング言語レベルの説明はScalaを対象にします。

Disclaimer

2008年にScalaをはじめて足掛け4年、関数型プログラミングとは、どうも数学を使ってプログラミングしていくことらしい、ということが分かってきました。

ScalaをBetter Javaとして使うのであれば、そこまで頑張らなくてもよいのですが、関数型言語のパワーを引き出すにはやはり関数型プログラミング、さらにいうと関数合成をベースとしたMonadicプログラミングをしていく必要があります。

ボク自身はにわか関数型プログラマですし、数学や計算機科学は門外漢なので関数型言語の背景技術を調べるのはなかなか辛いのですが、関数型プログラミングをする以上は避けて通れないので、牛歩のようなスピードですが、少しづつ調べています。

現時点で分かったことをまとめたのが上の図です。

計算機科学、数学の観点からは緩い点もあると思いますが、逆にオブジェクト指向プログラマの目から見た関数型言語という観点で見て頂けると、これから関数型言語にアプローチする人にはよいスタートポイントになるかもしれません。

Curry-Howard対応

プログラミング言語の中での関数型言語の位置付けを考える上で重要なのがCurry-Howard対応(Curry-Howard correspondence)です。

Curry-Howard対応の詳細は上記Wikiページやk.inabaさんのCurry-Howard Isomorphism も参考になります。

ざっくりいうと、「単純型付ラムダ計算」と「直感主義命題論理&自然演繹」がIsomorphism(同型)ですから文字通り相互変換が可能ということです。この枠組みの中では「型=命題」、「計算=証明」となり、コンパイルが成功すれば証明完了となります。まさにプログラミングが数学の証明と同じということです。凄いですね。

残念ながら「直感主義命題論理&自然演繹」で記述できる範囲は狭いので、汎用的に一般の問題を記述できるわけではありません。

しかし、同型という形で数学と直結している点が重要です。「直感主義命題論理&自然演繹」を軸に、数理論理学の膨大な理論体系をプログラミングに取り込む可能性がみえてきます。

純粋関数型言語

関数型言語は大きく純粋関数型言語と(純粋でない普通の)関数型言語に分類することができます。

純粋関数型言語は、副作用がないといった性質が有名?ですが、重要なのは「単純型付ラムダ計算」をプログラミング言語として実現した物ということであろうと思います。

「純粋関数型言語」=「単純型付ラムダ計算」であればさらに、「純粋関数型言語」=「直感主義命題論理&自然演繹」であるわけで、プログラミング言語と数理論理学が直結することになります。

ハードウェアの壁

純粋関数型言語は、「マッカーシーの5つの基本関数」 の時代から関数型言語の理想の姿でした。関数型言語における関数の評価は、ラムダ計算を基盤にしていて、数学的に記述して証明できることがそもそもの存在理由だったわけです。

しかし、ハードウェア性能の壁を乗り越えることは難しく、現在に至っています。関数型言語は実用性能を得るために、手続き型言語機能やオブジェクト指向言語機能を取り込んで、事実上関数型言語的な手続き型言語、関数型言語的なオブジェクト指向言語として使用されてきました。

いうまでもありませんが、ハードウェア性能の向上はこういった制約を取り払いつつあります。

Webアプリケーションなどのアプリケーションプログラムでは、RubyやPythonといったインタープリタ型のスクリプト言語が実用言語として十分機能することが明らかになってきました。コンパイル型の純粋関数型言語ではこの壁はすでに超えていることは容易に推測できます。

Scalaの立場

ハードウェア性能が純粋関数型言語を動作させるのに十分なレベルに向上してきたのではないかということを説明しました。しかし、それでは一足飛びに純粋関数型言語に切り替えてしまえばよいのかというと、そう簡単でもありません。

ハードウェア性能が向上したといっても、やはりぎりぎりの局面では副作用のあるプログラムにしたいケースは残りそうです。仮にそういう事は事実上は滅多にないにしても、いざという時のために保険の意味で機能は残しておきたいのが人情です。

もう一つは、純粋関数型言語で状態を扱う技術であるモナドの難易度が高いことです。習得コストがかなり高いので、すでにオブジェクト指向言語で普通にプログラミングできるエンジニアが、コストをかけて学ぶメリットがあるのかという問題が出てきます。モナドを習得してはじめてスタートラインに立てるというだけなので、習得のインセンティブはかなり小さくなります。

これらのニーズを満たす解として考えらられるのがScalaが採用しているハイブリッド方式です。

Scalaでは、キーワードval、mutable/immutableの2種類がパラレルに用意されているコレクションライブラリ、代数的データ型を実装するのに適したcase classとキーワードsealed、といった形で副作用のない純粋関数型的なプログラミングを行うための仕掛けが多数用意されています。

この範囲でプログラミングしておけば、純粋関数型言語に近しい効果を得ることができます。

Scalaでは不変オブジェクトであるListは何も設定しなくても使えるのに対して、オブジェクト指向プログラミングに必須のArrayBufferは、importしなければ使えないようになっています。これは、純粋関数型的な利用方法を推奨するのがScalaのスタンスということの現れだと思われます。もちろん「純粋」では対処できない問題に対して(Javaと同等以上の)通常のオブジェクト指向プログラミングも可能になっています。

また、アルゴリズムのコアの部分は純粋関数型的に記述するとしても、それを取り囲む周辺機能は普通のオブジェクト指向プログラミングで記述するのも可能です。このため、関数型プログラマとオブジェクト指向プログラマが同一言語で協業するような運用も可能になります。

もちろん、不慮のバグで副作用が発生する可能性もあるので、純粋関数型言語のように安全というわけではありませんが、プログラマが気をつければ、純粋関数型プログラミングのメリットを享受できるようになっているわけです。

新しい要素技術

純粋関数型言語と相性のよい色々な要素技術が追加されています。

  • 代数的データ型
  • 永続データ構造
  • 型クラス

代数的データ構造は、代数的な計算に適したデータ型です。Scalaでの実装は不変オブジェクトであることに加えてキーワードsealedを用いることで、開いた形での継承によるポリモーフィズムを使わないようにします。

永続データ構造は、副作用を持たないデータ構造です。純粋関数型データ構造(pure functional data structure)という用語もあり、同じ意味を持つと思われます。一度作ったデータ構造は、二度と変更されることがなくメモリ上に残り続けるので「永続」と呼ばれています。(不揮発性のストレージに格納するという意味の「永続」ではありません。)不変オブジェクトのみで木構造やグラフ構造といったものを実現します。関数型プログラミングでは永続データ構造を使うスキルが重要になってきます。

型クラスは、「アドホック・ポリモーフィズムを提供する型システム」と定義されています。

オブジェクト指向的にいうと、アルゴリズムを実現したフレームワークと処理対象のデータオブジェクトを、それぞれ相互に依存しない形で実装したものを、どちらも変更することなく後付で接続するためのメカニズムです。

Scalaでは暗黙パラメタを使ったConceptパターンという手法で実現します。暗黙パラメタに対する文脈として型クラス・インスタンスをバインドすることで、フレームワークとデータオブジェクトをプログラム上は疎結合のままコンパイル時に接続できるのがミソです。

群論と圏論

型クラス導入前から、Scalaでもモナドを使うことはできましたが、コンベンションを使用した決め打ちの実装で、拡張性に乏しいものでした。

モナド以外にも、関数型プログラミングに有用な抽象代数学の概念はたくさんあるので、これらを必要に応じて取り入れていきたいニーズがあります。この目的に適したメカニズムが型クラスです。

型クラスは純粋関数型言語であるHaskellが導入した言語機能で、Haskellのクラスライブラリで代数的なメカニズムを構築するのに使用されています。

Scalaでは、型クラスを実現するクラスライブラリScalazを用いると、群論(モノイドなど)や圏論(モナドなど)が定義しているさまざまな代数的な処理をプログラミングテクニックとして利用することが可能になります。

代数的な計算メカニズムのサポートは、当然ながら型付ラムダ計算とも相性がよく、さらに他の数学分野との連携でも有効に機能すると思われます。

発展の方向

数理論理学では、命題論理は基本中の基本ですが、述語論理や様相論理といった形で、よる複雑で応用範囲の広い理論が存在しています。

現段階では、ボクの調べた範囲では、述語論理や様相論理を関数型プログラミングの基本テクニックとして活用できるようにはなっていないようです。

とはいえ、最近話題の証明駆動という形になるのか、論理型言語に昇華していくのか、着地点は分かりませんが長い目で見ればいずれそういう方向に進むのではないかと思います。

圏論の上に論理学の圏を載せたものとしてトポスという理論体系があるようです。また、論理学と代数の裏側には常に集合論が見え隠れしています。このあたりの相互に関連を持つ数学の理論群がいろいろな形で関数型プログラミングにも取り入れられていくことが期待できます。

プログラミング言語が数学と直結することで、数学の理論体系がある意味、プログラムから利用できる具体的な機能となるわけです。実際にモナドやモノイドといった数学上の概念が型クラスという形で利用可能になりました。このポテンシャルはかなり大きく、今後プログラミング技術が大発展するホットスポットになるのではないかと思います。

Scala Tips / Either (13) - 二項演算, AND, Monoid

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

今回は、「Either:AND」×「値:Monoid」を考えます。

前回は、「Either:AND」×「値:任意の関数で計算」だったので、値の計算方法を「任意の関数で計算」から「Monoid」に変えたものになります。モノイド(Monoid)は、結合的な二項演算と単位元という性質を持つオブジェクトで、関数型プログラミングで頻出します。

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

EitherのAND
lhsrhs結果Rightの値Leftの値
RightRightRight二項演算-
RightLeftLeft-rhs
LeftRightLeft-lhs
LeftLeftLeft-二項演算

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

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

Scala標準ライブラリではMonoidは提供されていないので、この組み合わせが可能なのはScalazの場合です。以下では、比較の目的でJava風、Scala風、Scalaの項では(Monoidではなく)Int型に対して「+」を適用するコードを示します。そして、Scalazの項でMonoidを使った実装を行います。

(分類の基準)

Java風

if式を使って、4つの場合を記述します。Scalaの標準クラスライブラリにはモノイドがないので、ここではInt型の引数に対する二項演算は「+」の決め打ちにします。

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

Scala風

match式を使って、4つの場合を記述します。やはり、Int型に対する二項演算は「+」の決め打ちにします。

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

Tupleを用いてネストしない書き方の場合も、二項演算に「+」を決め打ちします。

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

Scala

RightProjectionのflatMapメソッドとmapメソッドを使うのがScala的なコーディングです。この場合も、二項演算に「+」を決め打ちします。

def f(e1: Either[Throwable, Int], e2: Either[Throwable, Int]): Either[Throwable, Int] = {
  e1.right.flatMap(e1r => e2.right.map(e2r => e1r + e2r))
}
for

for式を使うと2つのEitherに対する二項演算を簡潔に記述することができます。この場合も、二項演算に「+」を決め打ちします。

def f(e1: Either[Throwable, Int], e2: Either[Throwable, Int]): Either[Throwable, Int] = {
  for {
    e1r <- e1.right
    e2r <- e2.right
  } yield e1r + e2r
}

Scalaz

Scalaの標準ライブラリではMonoidは提供されていません。ここまでの説明では、Monoidを使わずInt型に対する「+」を決め打ちで使用しました。

ScalazではMonoidを使うことができるので、Monoidを使って実装していきます。また、Int型だけでなくMonoidであるオブジェクトではすべて同じロジックを適用するので、引数の型を型パラメータTとしました。型パラメータTは、コンテキスト・バウンドを使って「T: Monoid」としていますが、これは型パラメータTがMonoid型であることを指定しています。

ScalazではMonoid同士の加算演算として、演算子「|+|」を用意しているので、これを使います。

Scalazでは、RightProjectionだけではなくEitherも成功/失敗文脈のモナドとして使えるのと、flatMapメソッドとして>>=メソッドを使うことができるので、以下のようになります。

def f[T: Monoid](e1: Either[Throwable, T], e2: Either[Throwable, T]): Either[Throwable, T] = {
  e1 >>= (e1r => e2.map(e2r => e1r |+| e2r))
}

Scalazでは、多くのオブジェクトがMonoid型として定義されているので、Monoidに対するロジックをそのまま適用することができます。以下は実際に動いている様子です。

scala> f(1.right, 2.right)
res64: Either[Throwable,Int] = Right(3)

scala> f("one".right, "two".right)
res65: Either[Throwable,java.lang.String] = Right(onetwo)

scala> f(List(1, 2).right, List(3, 4).right)
res66: Either[Throwable,List[Int]] = Right(List(1, 2, 3, 4))

scala> f(true.right, true.right)
res69: Either[Throwable,Boolean] = Right(true)

scala> f(BigInt("1").right, BigInt("2").right)
res71: Either[Throwable,scala.math.BigInt] = Right(3)

scala> f((1, 2).right, (3, 4).right)
res74: Either[Throwable,(Int, Int)] = Right((4,6))

scala> f(Map(1 -> 10, 2 -> 20).right, Map(3 -> 30, 4 -> 40).right)
res75: Either[Throwable,scala.collection.immutable.Map[Int,Int]] = Right(Map(3 -> 30, 4 -> 40, 1 -> 10, 2 -> 20))
for

for式でもrightメソッドでRightProjectionを取り出す処理は省略できます。

Monoid同士の加算演算として、オペレータ「|+|」を使います。

def f[T: Monoid](e1: Either[Throwable, T], e2: Either[Throwable, T]): Either[Throwable, T] = {
  for {
    e1r <- e1
    e2r <- e2
  } yield e1r |+| e2r
}
Applicative Functor

Scalazでは、Applicative Functorを使って、2つ(またはそれ以上)のEitherに対して二項演算(N項演算)することができます。

Monoid同士の加算演算として、演算子「|+|」を使います。演算子「|+|」を使う場合には、「(e1 |@| e2)(f)」といった形でメソッド名のみを指定する省略形は使えないので、引数を指定する必要があります。

def f[T: Monoid](e1: Either[Throwable, T], e2: Either[Throwable, T]): Either[Throwable, T] = {
  (e1 |@| e2)(_ |+| _)
}

e1とe2が共にRightの場合、関数fの第1引数にe1(Right)の値、第2引数e2(Right)の値を適用して評価し、その結果をRightに詰めて返すという動作をします。

ノート

モノイド(monoid)は数学の代数学由来の性質です。モノイドは数学の色々な場所で使われる重要な性質のようで、数学的な観点できちんとみていくと非常に難解です。モノイドというと耳慣れないせいもあってなんだか仰々しですが、そういう意味で仰々しいに足る理由はあるわけですね。

ただし、関数型プログラミングにおいては、それほど恐れることはありません。オブジェクト指向プログラミングにおけるIteratorやFactoryと同様にイディオム、パターンの名前と気楽に考えておくとよいでしょう。プログラミングのテクニックとしては難しいものではありません。

ただし、その応用範囲は非常に広く、関数型プログラミングをする上では必須のパターンということができます。

ScalazにおけるMonoidは、加算的な演算一般を意味し、演算子は本文にも出てきた「|+|」や数学記号の「⊹」を用います。本文では「|+|」を使って色々なオブジェクトに対して共通のロジックを適用することができました。

また、Monoidは単位元を持っているという性質もあり、この性質を用いたテクニックもあります。たとえば、「Option (8)」ではMonoidの単位元をデフォルト値として使用しました。

型クラスとコンテキスト・バウンド

Monoidバージョンのプログラムでは、Int型ではなくて、コンテキスト・バウンドの記述方法[T: Monoid]を用いてMonoid型のオブジェクトを処理対象として宣言しました。

Scala言語そのものは、型クラスという文法は持っていませんが、暗黙パラメタを使ったConceptパターンという手法で、型クラスの機能が実現可能になっています。Scalazはこの手法を用いた型クラスのクラスライブラリというわけです。

コンテキスト・バウンドを使わず、暗黙パラメタを使って、本文の処理を定義すると以下のようになります。

def f[T](e1: Either[Throwable, T], e2: Either[Throwable, T])(implicit m: Monoid[T]): Either[Throwable, T] = {
  (e1 |@| e2)(_ |+| _)
}

こちらが本来の書き方で、コンテキスト・バウンドはその文法糖衣です。どちらの書き方も意味は変わらないので好きな方を用いるとよいでしょう。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

2012年2月28日火曜日

Scala Tips / Either (12) - Applicativeの記述方法

Applicativeの使用例として以下のものあげました。これは、Scala文法と相性のよい糖衣記法的なものです。

(e1 |@| e2)(f)

普通はこの糖衣記法を用いておけばよいのですが、同じ意味を持つ記述方法が色々あるので、参考のために以下で説明していきます。以下の記法の方が、Applicativeの本来の意味に近いので、Applicativeの振舞いを理解するには役に立つと思います。

演算子<*>

Applicativeの基本となる演算子は <*> です。式「コンテナ上に持ち上げられた値 <*> コンテナ上に持ち上げられたカリー化された関数」を評価すると「コンテナ上に持ち上げられた値」または「コンテナ上に持ち上げられたカリー化された関数」が得られます。

def f(e1: Either[Throwable, Int], e2: Either[Throwable, Int], f: (Int, Int) => Int): Either[Throwable, Int] = {
  e2 <*> (e1 <*> f.curried.right)
}

この例では、以下の処理が行われています。

まず「f.curried」で関数fをカリー化し、「right」でEither上に持ち上げています。その結果得られた「Either上に持ち上げられたカリー化された関数f」( Right(Int ⇒ Int ⇒ Int) )をe1(Either上に持ち上げられた値)に適用すると「Either上に持ち上げられたカリー化された関数」( Right(Int ⇒ Int) )が得られます。これをさらにe2(Either上に持ち上げられた値)に適用すると「Either上に持ち上げられた値」( Right(Int) )が得られます。

文章では分かりづらいですが、REPLで実行すると以下のような形になります。

scala> (1.right <*> (2.right <*> ((_: Int) + (_: Int)).curried.right[Throwable]))
res51: Either[Throwable,Int] = Right(3)

カリー化された関数が、右側からコンテナ上の値をパクパク、パックマンのように食べていくとイメージすると分かりやすいかもしれません。

mapメソッドで持ち上げ

上の例では最初に関数をカーリ化した上で(curried)、コンテナ上に持ち上げて(right)、最初のコンテナに適用する( <*> )という処理が走ります。この中で、「コンテナ上に持ち上げて(right)、最初のコンテナに適用する( <*> )」の部分はmapメソッドの動きそのものです。そこで、mapメソッドを使って関数のコンテナへの持ちげと関数の適用を同時に記述するのがイディオムになっています。

def f(e1: Either[Throwable, Int], e2: Either[Throwable, Int], f: (Int, Int) => Int): Either[Throwable, Int] = {
  e2 <*> (e1 map f.curried)
}

2引数関数

引数の数が2つの関数の場合は、演算子 <**> を使うことができます。この場合は、演算子 |@| と同じです。

def f(e1: Either[Throwable, Int], e2: Either[Throwable, Int], f: (Int, Int) => Int): Either[Throwable, Int] = {
  (e1 <**> e2)(f)
}

ただし、引数の数が3以上になるとちょっと事情が変わってきます。

演算子 |@| を使う場合には、以下のように自然な記法を使うことができます。引数の数が4以上になっても同じです。

(e1 |@| e2 |@| e3)(f)

一方、引数の数が3個用の演算子 <***> もありますが、記述方法が以下のものになります。若干、ぎこちない感じですね。

(e1 <***> (e2, e3))(f)

引数の数が4個用の演算子 <****> 、5個用の演算子 <****> もありますが、6個以上のものは用意されていません。

<*> 系の記述は引数が3個以上になると記述がぎこちなくなるのと、6個以上の引数のものが用意されていないことから、若干使いづらいものになっています。そういう意味で |@| を使うのが楽で良いのですが、|@| は内部的には少し複雑な動きをするので性能的には若干不利と思われます。

<*> 系と |@| は前者が若干性能が良く、後者が若干見た目がよい、覚えやすいという長所を持っています。いずれにしても微差なので好みで使い方を決めてよいでしょう。

ノート

「e2 <*> (e1 <*> f.curried.right)」では、関数をEitherの文脈に持ち上げるところを「f.curried.right」とrightメソッドを使っています。

rightメソッドはEitherの専用メソッドなので、汎用的な目的には使えません。ここをもう少し汎用的に書く場合はpureメソッドを使います。ただし、「f.curried.pure」とすると型情報が不足するためコンパイルエラーとなります。

このため以下のように適切な型情報を設定する必要があります。

def f(e1: Either[Throwable, Int], e2: Either[Throwable, Int], f: (Int, Int) => Int): Either[Throwable, Int] = {
  e2 <*> (e1 <*> f.curried.pure[({type λ[α]=Either[Throwable, α]})#λ])
}

「({type λ[α]=Either[Throwable, α]})#λ」は型パラメタの部分適用的な指定をする時のイディオムです。λやαといったギリシャ文字を使っていて仰々しいですが、これはこういう書き方が一般的なのでそうしています。({type X[A]=Either[Throwable, A]})#X」でも意味は同じです。

このような用途向けにScalazではPartialApply1Of2トレイトを用意しているのでこれを用いることもできます。

def f(e1: Either[Throwable, Int], e2: Either[Throwable, Int], f: (Int, Int) => Int): Either[Throwable, Int] = {
  e2 <*> (e1 <*> f.curried.pure[PartialApply1Of2[Either, Throwable]#Apply])
}

また、自分でtypeを定義してこれを指定することもできます。

def f(e1: Either[Throwable, Int], e2: Either[Throwable, Int], f: (Int, Int) => Int): Either[Throwable, Int] = {
  type EitherThrowable[A] = Either[Throwable, A]
  e2 <*> (e1 <*> f.curried.pure[EitherThrowable])
}

ただ、型を指定するのは煩雑なので、Applicativeの用途では「e2 <*> (e1 map f.curried)」というかたちでmapメソッドを使ったり、演算子 <**>|@| を使うのが実用的な選択となります。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

2012年2月27日月曜日

Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標

3月19日に要求開発アライアンスの3月定例会で『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』というタイトルでお話させていただくことになりました。

今回の定例会のテーマは『要求開発×クラウド』で丸山先生の『エンタープライズ・クラウドの現在』との二本立てとなっています。

内容は以下のようなものになる予定です。個人的にはクラウド、FPの文脈を取り込んだOFADの大枠みたいなものが朧気ながらみえてきた気がするので、各種の論点についてOOADの専門家の皆さんのご意見をお聞きできればという感じで考えています。

  • OOADの問題点
  • 最新FP with Scala
    • trait, monad, type class
  • クラウドプラットフォーム
  • モデリング&アーキテクチャ
    • DDD, DCI, CQRS, EDA
  • その他の技術動向
    • DSL, コード中心, Agile/Lean, SA/SD
  • OFP = OOP+FP
  • OFAD
  • OFAD with Scala

セッション時間は50分ですが、OFADの要素技術、関連技術が多岐に渡るため個別の技術についてはOFADの文脈における枠組みを示すにとどまることになります。

今までは、こういった場合、スライドを作りすぎて駆け足になってしまったり、作ったスライドを捨てたりすることが多く、なかなか情報を的確に伝えきれないというのが反省点になっていました。駆け足で説明をはしょったり、削った項目についてはその場ではブログでまとめておくことを考えたりするのですが、セッションが終わってしまうと集中力が切れるためか、結局そのまま放置ということになってしまっています。

そこで、今回は個別の技術については、準備ができたものについては事前にブログ上に説明を上げておくことにしました。セッション(スライドはセッション後に公開予定です)と合わせてより包括的に情報を伝達することができればと思います。

Scala Tips / Either (11) - Applicative

Either (10) - 二項演算, AND」でApplicative Functor(以下Applicative)という型クラスを使いました。見慣れない演算子「|@|」が使われていますが、これがScalazが提供するApplicativeの演算子です。

def f(e1: Either[Throwable, Int], e2: Either[Throwable, Int], f: (Int, Int) => Int): Either[Throwable, Int] = {
  (e1 |@| e2)(f)
}

e1とe2が共にRightの場合、関数fの第1引数にe1(Right)の値、第2引数e2(Right)の値を適用して評価し、その結果をRightに詰めて返すという動作をします。

逆にいうと、e1とe2のどちらかがLeftの場合、結果もLeftになります。つまり、Eitherを成功/失敗文脈として、その上でEitherの持つ値に対して関数fを適用しています。

「|@|」が引数の区切り記号のようなイメージで使われていて、動作のイメージとしては「f(e1, e2)」となります。ただし、失敗の文脈の処理は文脈の中で自動的に行ってくれるわけです。

このように文脈上の処理(Eihterの場合は失敗文脈の引き継ぎ)を自動的に行なってくれる点が、「f(e1, e2)」という形で関数を生で使うのではなく、「(e1|@|e2)(f)」という形でApplicativeを使うメリットです。

Functor, Applicative, Monad

ScalazではFunctorとMonadの間に、Functorより強力で、Monadよりシンプルな型クラスApplicativeを導入しています。型クラスFunctorのサブ型クラスがApplicative、Applicativeのサブ型クラスがMonadという関係になっています。

以下では、Functor, Applicative, Monadの機能比較と使い分けについて簡単に説明します。説明のために、必要に応じてFunctor、Applicative、Monadの事をコンテナと呼びます。それぞれ「Functorのコンテナ」、「Applicativeのコンテナ」、「Monadのコンテナ」というぐらいの意味をイメージしながら読み進めてください。

Functor

Functorは、引数が1つの関数をコンテナに適用することができますが、複数のコンテナに対して複数の引数を持つ関数を適用することはできません。一方、Functorの合成は簡単に行うことができます。

Monad

Monadは、複数のコンテナに対して複数の引数を持つ関数を適用することができます。一方、Monadの合成はできないことはありませんが、モナド変換子やクライスリ圏上での射を導入する必要があり、かなり難易度が上がります。

Applicative

Functorは合成は簡単にできるものの複数のコンテナに対して複数の引数を持つ関数を適用することができません。一方、Monadは複数のコンテナに対して複数の引数を持つ関数を適用することはできすが、合成を行うのがなかなか面倒です。

そこで、登場するのがApplicativeです。Applicativeは、複数のコンテナに対して複数の引数を持つ関数を簡単に適用することができるのが特徴です。

前述の以下の使い方は、e1(Either)とe2(Either)という2つのコンテナに対して、引数2の関数fを適用していますが、まさにApplicativeの典型的な使用方法です。

(e1 |@| e2)(f)

またApplicativeの合成は比較的簡単に行うことができます。これはMonadに対する優位点です。

ただし、Applicativeでは、文脈の切替えを行うことができません。文脈の切替えが必要な場合はMonadを使う必要があります。

比較

Functor, Applicative, Monadの性質を比較すると以下のものになります。

型クラス複数引数関数×複数コンテナ合成文脈の切替
Functor
Applicative
Monad

使い分けですが、以下の方針がよいでしょう。

まず、文脈の切替が必要な場合はMonadを選択するしかありません。また、1引数関数を1コンテナに適用する場合は、簡潔に記述できるFunctorがよいでしょう。

問題は「複数引数関数×複数コンテナ」の場合です。

まず、記述方法の簡潔度ですが、ケースバイケースといえます。Monadの場合flatMapメソッドや>>=メソッドを使った記述はちょっと煩雑ですが、(Monad演算の文法糖衣である)for式が適用できる場合には簡潔に記述することができます。ただし、Applicativeの「|@|」の記法がぴったりはまるケースはより簡潔に記述することができます。このあたりは好みの問題もあるので、好きな方を選べばよいでしょう。

残るのはコンテナの合成ですが、コンテナの合成が必要、あるいは含みを持たせたい場合には、Applicativeにしておくのが有利です。つまり、文脈の切替がないケースで、コンテナの合成がある/ありそうな場合にApplicativeを選択することになります。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

2012年2月24日金曜日

Scala Tips / Either (10) - 二項演算, AND

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

今回は、「Either:AND」×「値:任意の関数で計算」を考えます。

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

EitherのAND
lhsrhs結果Rightの値Leftの値
RightRightRight二項演算-
RightLeftLeft-rhs
LeftRightLeft-lhs
LeftLeftLeft-二項演算

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

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

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

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

lhs/rhsともRight
任意の関数で計算
lhs/rhsともLeft
lhs側を使う

(分類の基準)

Java風

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

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

Scala風

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

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

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

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

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

Scala

RightProjectionのflatMapメソッドとmapメソッドを使うのがScala的なコーディングです。

def f(e1: Either[Throwable, Int], e2: Either[Throwable, Int], f: (Int, Int) => Int): Either[Throwable, Int] = {
  e1.right.flatMap(e1r => e2.right.map(e2r => f(e1r, e2r)))
}

「lhs/rhsともLeftの場合はlhs側を使う」という選択を行うと、失敗の文脈の処理をflatMapメソッドに任せることができるようになります。このため、アプリケーションロジックはflatMapに渡す関数のみを考えればよいわけです。

for

for式を使うと2つのEitherに対する二項演算を簡潔に記述することができます。

def f(e1: Either[Throwable, Int], e2: Either[Throwable, Int], f: (Int, Int) => Int): Either[Throwable, Int] = {
  for {
    e1r <- e1.right
    e2r <- e2.right
  } yield f(e1r, e2r)
}

for式はモナドに対する文法糖衣で、実際の動きは前述の以下のものと同じです。

e1.right.flatMap(e1r => e2.right.map(e2r => f(e1r, e2r)))

for式を使うと簡潔に記述できるのでうまく活用したいですね。

Scalaz

Scalazでは、RightProjectionだけではなくEitherも成功/失敗文脈のモナドとして使えるのと、flatMapメソッドとして>>=メソッドを使うことができるので、以下のようになります。

def f(e1: Either[Throwable, Int], e2: Either[Throwable, Int], f: (Int, Int) => Int): Either[Throwable, Int] = {
  e1 >>= (e1r => e2.map(e2r => f(e1r, e2r)))
}
for

for式でもrightメソッドでRightProjectionを取り出す処理は省略できます。

def f(e1: Either[Throwable, Int], e2: Either[Throwable, Int], f: (Int, Int) => Int): Either[Throwable, Int] = {
  for {
    e1r <- e1
    e2r <- e2
  } yield f(e1r, e2r)
}
Applicative Functor

Scalzでは、Applicative Functorを使って、2つ(またはそれ以上)のEitherに対して二項演算(N項演算)することができます。

def f(e1: Either[Throwable, Int], e2: Either[Throwable, Int], f: (Int, Int) => Int): Either[Throwable, Int] = {
  (e1 |@| e2)(f)
}

e1とe2が共にRightの場合、関数fの第1引数にe1(Right)の値、第2引数e2(Right)の値を適用して評価し、その結果をRightに詰めて返すという動作をします。

ノート

Eitherを純粋な意味で直和(disjoint union)、選択(choice)として使う場合はLeftの場合もRightの場合も値に対する二項演算を考えることになります。

しかし、ここではEitherを成功/失敗文脈として使うケースに絞っています。この場合には、アプリケーションロジックは成功文脈の上で実行することになり、成功文脈であるRight側が軸となります。

それに対して失敗文脈であるLeftは定型的な処理で簡単に流すこと、理想的にはアプリケーション側に処理を意識させないことが重要になります。その目的で、今回は「lhs/rhsともLeftの場合はlhs側を使う」、すなわちエラーが発生した時点でエラーモードになって後の演算は行わないという扱いにしました。これは、Eitherに対する二項演算という観点ではLeftを偽とする短絡評価AND、値に対する二項演算という観点では短絡評価ORの動きともいえます。

flatMap

Either (2) - flatMapでは、成功の文脈と失敗の文脈を切り替える目的でflatMapメソッドを使用しました。

今回は、成功の文脈と失敗の文脈の切替えは行っていませんが、flatMapを使っています。つまり、flatMapには文脈を切り替える以外の利用方法があるということですね。

flatMapには、2つのモナドを結合(join)するという重要な機能があります。

RightProjection(Scalazの場合はEither本体も同様)のflatMapメソッドは以下のような実装になっています。

def flatMap[AA >: A, Y](f: B => Either[AA, Y]) = e match {
      case Left(a) => Left(a)
      case Right(b) => f(b)
    }

RightProjectionの結合ロジックは、(1)Leftだったら何もせずそのままLeftを返す(関数fが作るはずのEitherは捨てる)、(2)Rightだったら関数fに新しいEitherを作ってもらったものをそのまま返す(自分自身は捨てる)、となります。このようにして、どちらかを捨てるというロジックで2つのEitherを結合します。この結合ロジックがアプリケーションロジックの意図と同じ場合には、flatMapメソッドを利用することができるわけです。

「lhs/rhsともLeftの場合はlhs側を使う」という選択は、RightProjection#flatMapメソッドの動きを意識してものでした。

Applicative Functor

Applicative Functorに関する話題は次回に説明する予定です。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

2012年2月23日木曜日

Scala Tips / Either (9) - 二項演算

ここまで、1つのEitherに対する演算について見てきました。今回から、2つのEitherに対する二項演算について考えていくことにします。

今回はEitherに対する二項演算のバリエーションについて整理します。

Eitherに対する二項演算を考える上では、EitherそのものとEitherに格納される値という2つのレイヤそれぞれに対する二項演算を考えなくてはなりません。

  • Eitherそのものに対する二項演算
  • 値に対する二項演算

以下で、それぞれについてみていきましょう。

Eitherそのものに対する二項演算

Eitherは、LeftまたはRightのいずれかなので、2つのEitherの組合せは以下の4パターンになります。以下では二項演算の第1引数(左側)をlhs(left hand side)、第2引数(右側)をrhs(right hand side)と表現します。左右という表現になっていますがEitherのLeft, Rightとは別のものなのでご注意ください。

Eitherの二項演算
lhsrhs
RightRight
RightLeft
LeftRight
LeftLeft

lhsとrhs共にRightまたはLeftの場合には、Right内またはLeft内に格納される値間の二項演算を行います。

問題なのが、lhsとrhsがRight/LeftやLeft/Rightのように別のものになる場合です。この場合は、Right/LeftとLeft/Rightの対に対してRightまたはLeftのどちらかを結果とするEitherレベルでの二項演算が必要になります。

スコープをEitherの慣用的な使い方である「Leftが失敗、Rightが成功の成功失敗文脈」とした上で、Leftを偽、Rightを真と考えると、Eitherレベルでの二項演算のセマンティクスとしては、16種類の論理演算が候補になります。

ここでは論理和(以下OR)と論理積(以下AND)の2種類を考えることにします。(排他的論理和(XOR)や含意(IMP)も面白そうです。このあたりを追求していくと面白いイディオムが見つかるかもしれません。)

Rightを成功文脈として考えた場合のEitherのORは以下のものになります。

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

またANDは以下のものになります。

EitherのAND
lhsrhs結果Rightの値Leftの値
RightRightRight二項演算-
RightLeftLeft-rhs
LeftRightLeft-lhs
LeftLeftLeft-二項演算

値に対する二項演算

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

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

 次回より、Eitherに対する二項演算、値に対する二項演算の組合せに対して具体的なイディオムを考えていきます。

2012年2月22日水曜日

Scala Tips / Either (8) - for, getOrElse

Eitherを成功/失敗の文脈で使用する方法のイディオムです。

Eitherでは、値を取り出す処理として以下の2つのコーディングパターンがあります。

  • Either[A, B]からEither[A, C]に変換
  • Eigher[A, B]からCに変換

前回は、前者についてfor式で取り出すイディオムについて見てきました。

今回は、後者についてfor式で取り出すイディオムについて見ていきます。

Either (3) - getOrElseEither (4) - getOrElse, flatMapでEitherを成功/失敗の文脈として使用し、値を取り出す方法について説明しました。

Either (3) - getOrElseは成功/失敗の文脈を切り替えない以下の演算:

条件結果演算
Either[A, B]がRight[A, B]CBからCを計算
Either[A, B]がLeft[A, B]Cデフォルト値

Either (4) - getOrElse, flatMapは成功/失敗の文脈を切り替える以下の演算です。

条件結果演算
Either[A, B]がRight[A, B]で有効な値が入っているCBからCを計算
Either[A, B]がRight[A, B]で無効な値が入っているCデフォルト値
Either[A, B]がLeft[A, B]Cデフォルト値

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

文脈切替なし

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

条件結果演算
Either[A, B]がRight[A, B]CBからCを計算
Either[A, B]がLeft[A, B]Cデフォルト値
Scala

Either[Exception, Int]からEither[Exception, String]へ変換は以下のようになります。

def f(a: Either[Exception, Int]): String = {
  (for (b <- a.right) yield b.toString) match {
    case Right(c) => c
    case Left(_) => ""
  }
}

for式でEither[Exception, Int]→Either[Exception, String]の処理を行い、結果をmatch式で取り出します。EitherはOptionのgetOrElseに相当するメソッドがないのでmatch式を使います。

Option

Eitherで説明したとおり、演算が有効でない場合にはデフォルト値を返す処理では遅かれ早かれエラー情報を捨てるので、計算文脈をOptionに切り替えて演算を進めるのが有用なテクニックです。

Optionに切り替えるタイミングは色々ありますが、演算の最後にOptionに切り替えると以下のようになります。

def f(a: Either[Exception, Int]): String = {
  (for (b <- a.right) yield b.toString).right.toOption getOrElse ""
}

また、最初の段階でOptionに切り替えると以下のようになります。

def f(a: Either[Exception, Int]): String = {
  (for (b <- a.right.toOption) yield b.toString) getOrElse ""
}

こちらのほうが簡明ですね。演算途中でエラー情報を使わない場合は、最初の段階でOptionに切り替えるとよいでしょう。

Scalaz & Option

Scalazを使うとgetOrElseメソッドの代わりに「|」を使うことができます。記述がさらに簡明になります。

def f(a: Either[Exception, Int]): String = {
  (for (b <- a.right.toOption) yield b.toString) | ""
}

文脈切替あり

Either (2) - flatMapで取り上げた成功/失敗の文脈を切り替える以下の演算です。

条件結果
Either[A, B]のRight[A, B]に有効な値が入っているRight[A, C]
Either[A, B]のRight[A, B]に無効な値が入っているLeft[A, C]
Either[A, B]がLeft[A, B]Left[A, C]
Scala

Intは0以上のものが有効という条件付きのEither[Exception, Int]からStringへ変換をfor式を使って記述すると以下のようになります。

def f(a: Either[Exception, Int]): Either[Exception, String] = {
  def g(c: Either[Exception, Int]): Either[Exception, Int] = {
    c.right flatMap { d =>
      if (d >= 0) Right(d)
      else Left(new IllegalArgumentException("bad"))
    }
  }
  (for (b <- g(a).right) yield b.toString) getOrElse ""
}

これは、前回のプログラムを拡張したものですが前回と同様に少し無理がありますね。

Option

Eitherの中身を値として取り出すときはOptionに変換する方法が有用です。Optionに変換してよいのであれば、for式のif句を使うことができます。

def f(a: Either[Exception, Int]): String = {
  (for (b <- a.right if b >= 0) yield b.toString) getOrElse ""
}

for式の結果がOptionで返されるのでgetOrElseメソッドで値を取り出します。

Scalaz & Option

if句を使う場合は、Scalazの場合もrightメソッドでRightProjectionを使います。

def f(a: Either[Exception, Int]): String = {
  (for (b <- a.right if b >= 0) yield b.toString) | ""
}

Scalazを使うとgetOrElseメソッドの代わりに「|」を使うことができます。記述がさらに簡明になります。

ノート

Eitherに対してfor式のif句を使用するとEitherがOptionに切り替わってしまいます。Monadic演算的にはちょっと困った感じもしますが、最終的に値を取り出す用途には都合がよい振舞いともいえます。

Eitherを使うときの技として覚えておくと便利です。

warning

for式のif句でEitherを使おうとすると以下の警告が出ます。

<console>:17: warning: `withFilter' method does not yet exist on Either.RightProjection[Exception,Int], using `filter' method instead
         (for (b <- a.right if b >= 0) yield b.toString) | ""
                      ^
f: (a: Either[Exception,Int])String

for式のif句はwithFilterメソッドを想定しているのに対して、Eitherはfilterメソッドしか用意していないため、withFilterメソッドの代替手段としてfilterメソッドを使っているという警告です。

この警告から、Eitherはあまり使われていないので、こういう細かい機能が熟(こな)れていないのではないか、という印象を受けます。

クラスライブラリのあまり熟(こな)れていない所の細かい機能を使うと何かとバグを踏んだり、仕様変更の被害を受けがちなので、Eitherに対するfor式のif句はあまり積極的には使わないほうがよいかもしれません。リスクを勘案した上で使用の有無を決めるとよいでしょう。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

2012年2月21日火曜日

SimpleModeler 0.3.3

モデルコンパイラSimpleModeler 0.3.3をリリースしました。
基本的にはマインドマップ(XMind)とCSVからクラス図を生成する処理が実用フェーズになっています。
SimpleModelerは、プログラム生成処理のフレームワークを新しいものに入れ替えたため、以前動いていた処理は動かなくなっています。
重要度の低いと思われる機能は削除予定です。(必要に応じて新フレームワーク上で再実装します。)

機能

Simplemodeler 0.3.3では以下のオプションを提供しました。
オプション機能状況
projectプロジェクト生成α
importモデル移入α
convertモデル変換試験的
html仕様書生成α
javaJava生成α
androidAndroid生成α
diagramクラス図生成
buildプロジェクトビルド試験的
gaejGoogle App Engine Java生成試験的
gaeGoogle App Engine Python生成試験的
gaeoGoogle App Engine Oil生成削除予定
grailsGrails生成試験的
g3g3生成試験的
asakusaAsakusa生成試験的
基本的にはマインドマップ(XMind)とCSVからクラス図を生成する処理が実用フェーズになっています。その他の機能はα版または試験的実装の状態です。

インストール

プログラムの配布は、Scalaで最近注目されているconscriptを使っています。conscriptのインストール方法は以下のページに詳しいです。
Linux, Macであれば、以下のようにすればインストール完了です。
$ curl https://raw.github.com/n8han/conscript/master/setup.sh | sh
conscriptをインストールした後、以下のようにしてSimpleModelerをインストールします。
$ cs asami/simplemodeler
以下のコマンドがインストールされます。
sm
SimpleModelerコマンド
$ sm -version
Copyright(c) 2008-2012 ASAMI, Tomoharu. All rights reserved.
SimpleModeler Version 0.3.3 (20120220)

使い方

マニュアルはまだありません。以前のバージョン用のものがありますが、機能が色々変わってしまったので一から見直す予定です。
リファレンスマニュアルとユーザーガイドの元ネタをこのブログで随時書いていきます。
クラス図生成
CSVまたはXMind(マインドマップ)からクラス図を生成することができます。
以下のCSVファイルをsample.csvとして用意します。
#actor,base
顧客
個人顧客,顧客
法人顧客,顧客
#resource,attrs,powers
商品,商品名;定価(long),商品区分(第1類;第2類;第3類)
#event,parts
購入する,顧客;商品
SimpleModelerを以下のように実行します。
$ sm -diagram sample.csv
以下のクラス図の画像が生成されます。

モデル記述に使用するCSVの文法は近いうちに説明する予定です。

Scala Tips / Either (7) - for

Eitherを成功/失敗の文脈で使用する方法のイディオムです。

Eitherでは、値を取り出す処理として以下の2つのコーディングパターンがあります。

  • Either[A, B]からEither[A, C]に変換
  • Eigher[A, B]からCに変換

今回は、前者についてfor式で取り出すイディオムについて見ていきます。

EitherEither (2) - flatMapでEitherを成功/失敗の文脈として使用する方法について説明しました。

Eitherは成功/失敗の文脈を切り替えない以下の演算:

条件結果
Either[A, B]がRight[A, B]Right[A, C]
Either[A, B]がLeft[A, B]Left[A, C]

Either (2) - flatMapは成功/失敗の文脈を切り替える以下の演算です。

条件結果
Either[A, B]のRight[A, B]に有効な値が入っているRight[A, C]
Either[A, B]のRight[A, B]に無効な値が入っているLeft[A, C]
Either[A, B]がLeft[A, B]Left[A, C]

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

文脈切替なし

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

条件結果
Either[A, B]がRight[A, B]Right[A, C]
Either[A, B]がLeft[A, B]Left[A, C]
Scala

Either[Exception, Int]からEither[Exception, String]へ変換は以下のようになります。Eitherそのものはモナドではないためfor式のジェネレーターに指定することはできません。右側が成功の文脈になるので、Either#rightメソッドでRightProjectionを取得し、これをジェネレーターに指定します。

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

Eitherで使用したmapメソッドと同様に、Either[Exception, Int]がLeft(Exception)だった場合の処理を書く必要がありません。

Scalaz

Scalazでは、Eitherが右側を成功文脈とするモナドに拡張されるのでfor式のジェネレーターに直接指定することができます。

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

文脈切替あり

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

条件結果
Either[A, B]のRight[A, B]に有効な値が入っているRight[A, C]
Either[A, B]のRight[A, B]に無効な値が入っているLeft[A, C]
Either[A, B]がLeft[A, B]Left[A, C]
Scala

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

def f(a: Either[Exception, Int]): Either[Exception, String] = {
  def g(c: Either[Exception, Int]): Either[Exception, Int] = {
    c.right flatMap { d =>
      if (d >= 0) Right(d)
      else Left(new IllegalArgumentException("bad"))
    }
  }
  for (b <- g(a).right) yield b.toString
}

Optionの場合と違って、Eitherの場合はif句で成功文脈から失敗文脈への切り替え条件を指定することはできません。if句を指定すると計算文脈がEitherからOptionに切り替わってしまうからです。

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

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

Scalaz

Scalazの場合も、基本的にはScalaの場合と同じでfor式を使うのは無理があるようです。

def f(a: Either[Exception, Int]): Either[Exception, String] = {
  def g(c: Either[Exception, Int]): Either[Exception, Int] = {
    c >>= { d =>
      (d >= 0) either new IllegalArgumentException("bad") or d
    }
  }
  for (b <- g(a)) yield b.toString
}

ノート

Optionのfor式で説明したとおり、Scalaのfor式はモナドによる演算の文法糖衣となっています。

このためEitherのRightProjectionまたはLeftProjectionをジェネレーターに指定することができます。また、ScalazではEitherが右側を成功文脈とする成功/失敗のモナドに拡張されるので直接ジェネレータに指定できるようになります。

Eitherをfor式で使用するときの鬼門はif句で、計算文脈をEitherからOptionに切り替えてしまうので、この条件に合致する場合でないと使うことができません。

このため、成功文脈から失敗文脈への切替ではif句を使わない方法を使用する必要があります。本記事では、ジェネレータに指定する段階で成功の文脈から失敗の文脈に切り替える方式で対応してみました。 本記事の例題のような簡単な処理の場合は、処理のバランス上、回避処理が大げさになってしまいます。for式を使うより、直接flatMapメソッド(Scalazの場合は>>=メソッド)で記述したほうがよいでしょう。

ただし、より複雑なMonadic演算をする場合には、本記事で行った回避策を用いてfor式を使ったほうがよい場合もあります。この話題はいずれ取り上げる予定です。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

2012年2月20日月曜日

Scala Tips / Either (6) - Left, Right

EitherのサブクラスであるLeftとRightを生成するイディオムです。

Scala

Left[Exception]とRight[Int]は以下のようにして生成します。

val a = Right(10)
val a = Left(new IllegalArgumentException("bad"))

LeftやRightは、Either型のメソッドの返却値として生成することが多く、メソッドのシグニチャで型が記述されている場合には、不足情報を型推論で補ってくれるため通常はこの生成方法でかまいません。

ただし、型推論が効かない局面では困った問題が起きてしまいます。たとえばRight(10)の型はEither[Exception, Int]ではなくRight[Nothing, Int]にLeft(new IllegalArgumentException("bad"))の型もEither[Exception, Int]ではなくLeft[java.lang.IllegalArgumentException,Nothing]になってしまいます。

scala> val a = Right(10)
a: Right[Nothing,Int] = Right(10)

scala> val a = Left(new IllegalArgumentException("bad"))
a: Left[java.lang.IllegalArgumentException,Nothing] = Left(java.lang.IllegalArgumentException: bad)

このため、型をEither[Exception, Int]にするためには以下のように変数定義に記述するか:

val a: Either[Exception, Int] = Right(10)
val a: Either[Exception, Int] = Left(new IllegalArgumentException("bad"))

オブジェクト側に型注釈を付ける必要があります。

val a = Right(10): Either[Exception, Int]
val a = Left(new IllegalArgumentException("bad")): Either[Exception, Int]

いずれもかなり冗長ですね。

Scalaz

Scalazでは、以下の記述方法でEither型のインスタンスとしてLeftとRightを生成することが可能です。

val a = 10.right[Exception]
val a = new IllegalArgumentException("bad").left[Int]

「10.right[Exception]」のように任意のオブジェクトのrightメソッド(Scalazが追加)によってEither[Exception, Int]型のインスタンスとしてRight[Exception, Int]を生成することができます。

leftの場合も基本的には同様ですが、上の例ではEither[Exception, Int]ではなくてEither[IllegalArgumentException, Int]になってしまいます。

scalazの場合も、このように指定する具象オブジェクトが、目的とする型のサブクラスの場合には、型注釈などを用いて陽に指定する必要があります。

scala> 10.right[Exception]
res1: Either[Exception,Int] = Right(10)

scala> new IllegalArgumentException("bad").left[Int]
res9: Either[java.lang.IllegalArgumentException,Int] = Left(java.lang.IllegalArgumentException: bad)

scala> new IllegalArgumentException("bad").left: Either[Exception, Int]
res10: Either[Exception,Int] = Left(java.lang.IllegalArgumentException: bad)

ノート

Right/Leftの生成方法については、Option(11) - Some/Noneも参考になると思います。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

2012年2月17日金曜日

Scala Tips / Either (5) - Exception

Eitherを成功/失敗の文脈で使用する方法のイディオムです。

前回はEitherから値を取り出す際に、以下の表の演算を行う方法について考えました。

条件結果
Either[A, B]のRight[A, B]に有効な値が入っているRight[A, C]
Either[A, B]のRight[A, B]に無効な値が入っているLeft[A, C]
Either[A, B]がLeft[A, B]Left[A, C]

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

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

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

(分類の基準)

Java風

if式でEither#isRightを使って値の有無を判定します。

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

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

def f(a: Either[Exception, String]): Either[Exception, Int] = {
  if (a.isRight) {
    try {
      Right(a.right.get.toInt)
    } catch {
      case e: NumberFormatException => Left(e)
    }
  } else a.asInstanceOf[Either[Exception, Int]]
}

Scala風

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

def f(a: Either[Exception, String]): Either[Exception, Int] = {
  a match {
    case Right(b) => {
      try {
        Right(b.toInt)
      } catch {
        case e: NumberFormatException => Left(e)
      }
    }
    case Left(b) => Left(b)
  }
}

Scala

Eitherのrightメソッドから得られるRightProjectionのmapメソッドを使うのがScala的なコーディングです。ただし、今回のケースでは例外をキャッチする処理を行う必要があります。

def f(a: Either[Exception, String]): Either[Exception, Int] = {
  try {
    a.right.map(_.toInt)
  } catch {
    case e: NumberFormatException => Left(e)
  }
}

mapメソッドの外側でtry/catchを使って例外をキャッチするのは、ちょっと美しくありません。例外に依存するロジックは部品化の妨げになるので、できるだけ局所化して隠蔽したいところです。

このためには、mapメソッドの中で例外をキャッチしたいわけですが、この場合は例外をキャッチしたときに、成功の文脈から失敗の文脈への切り替えを行う必要があるので、Either (2)で取り上げたEitherProjection#flatMapメソッドを使用します。

def f(a: Either[Exception, String]): Either[Exception, Int] = {
  a.right.flatMap { b => 
    try {
      Right(b.toInt)
    } catch {
      case e: NumberFormatException => Left(e)
    }
  }
}
scala.util.control.Exception

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

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

import scala.util.control.Exception._    

def f(a: Either[Exception, String]): Either[Exception, Int] = {
  a.right.flatMap { b => 
    catching(classOf[NumberFormatException]).either(b.toInt)
             .asInstanceOf[Either[Exception, Int]]
  }
}

ただ、残念なのがcatching関数の返す型がEither[Throwable, U]であるため、Either[Exception, Int]を返すと不整合を起こしてしまいます。そこで、ここではasInstanceOf[Exception, Int]を用いて回避するという残念な結果になりました。

allCatch

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

import scala.util.control.Exception._    

def f(a: Either[Exception, String]): Either[Exception, String] = {
  a.right.flatMap { b =>
    allCatch.either(b.toInt)
            .asInstanceOf[Either[Exception, String]]
  }
}

この場合も、Either[Throwable, U]をasInstanceOf[Exception, Int]を用いて回避しています。

Scalaz

Scalaz流のエレガントな書き方はないと思います。

ノート

Eitherがかなり扱いにくいオブジェクトであることを説明しましたが、例外処理においてもその問題が顕在化しています。

Eitherの問題は、scala.util.control.Exceptionのcatchingメソッド、allCatchメソッドで返されるCatchオブジェクトのeitherメソッドのシグネチャがThrowable固定になっていることに起因しています。あまりEitherが使われていないために、クラスライブラリにおけるEitherの扱いが熟(こな)れていないという印象を受けます。

eitherメソッドがEither[Throwable, U]が返すためにasInstanceOf[Exception, Int]を用いて対応しています。asInstanceOf[Exception, Int]は明らかに美しくないので回避するために、いろいろな方法(withApplyメソッドなど)を試してみましたが結局よい方法が見つかりませんでした。よい回避方法があれば教えてもらえるとうれしいです。

Eitherの使い方のテクニックとしては、Eitherを成功/失敗の計算文脈として使用する場合には、Eitherの左側、失敗文脈にはExceptionではなくThrowableを入れるという慣習にしておくとよいですね。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

2012年2月16日木曜日

Scala Tips / Either (4) - getOrElse, flatMap

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

Eitherから値を取り出す処理として以下の2つのコーディングパターンを挙げました。

  • Either[A, B]からEither[A, C]に変換
  • Eigher[A, B]からCに変換

前回は、後者のEither[A, B]からCへの変換について、基本的なイディオムを紹介しました。

条件結果演算
Either[A, B]がRight[A, B]CBからCを計算
Either[A, B]がLeft[A, B]Cデフォルト値

今回は、Either[A, B]がLeft[A, B]の場合だけでなく、Right[A, B]の値が条件を満たさない場合も、デフォルト値を返すようにします。

条件結果演算
Either[A, B]がRight[A, B]で有効な値が入っているCBからCを計算
Either[A, B]がRight[A, B]で無効な値が入っているCデフォルト値
Either[A, B]がLeft[A, B]Cデフォルト値

以下では、Either[Exception, Int]からStringへの変換を例に考えてみます。ただし、Intは0以上のものが有効という条件を追加します。Right[Int]に入っているIntが0以上の場合、Stringが処理結果となります。一方、0未満の場合は無効となり空文字列「""」が処理結果となります。またEither[A, B]がLeft[A, B]の場合も空文字列「""」が処理結果となります。

(分類の基準)

Java風

if式でEither#isRightを使って右側の値の有無を判定します。デフォルト値の「""」はelse句で指定します。isRightメソッドが一回、getメソッドが二回、泣き別れになってしまいます。

def f(a: Either[Exception, Int]): String = {
  if (a.isRight && a.right.get.toString >= 0) {
    a.right.get.toString
  } else ""
}

Scala風

match式を使うと以下のようになります。こちらの方が綺麗です。

def f(a: Either[Exception, Int]): String = {
  a match {
    case Right(b) if b >= 0 => b.toString
    case _ => ""
  }
}

Scala

EitherでMonadic演算をしたい場合にはright(またはleft)メソッドでRightProjection(またはLeftProjection)を取得し、これに対してmapメソッドやflatMapメソッドを適用します。EitherとRightProjection/LeftProjectionの組合せでモナド的な動作をします。

前回は計算文脈の切り替えは行わなかったのでmapメソッドを使いましたが、今回は計算文脈の切り替えを行うのでflatMapメソッドを使います。

以下ではEitherからrightメソッドでRightProjectionを取り出しflatMapメソッド内で、Right[Exception, Int]の値が0未満である場合はLeft(new IllegalArgumentException("bad"))を返すことで失敗の計算文脈に切り替える処理を行っています。

def f(a: Either[Exception, Int]): String = {
  a.right.flatMap { b =>
    if (b >= 0) Right(b.toString)
    else Left(new IllegalArgumentException("bad"))
  }.right getOrElse ""
}
Optionに変換

最終的に値を取り出す場合、左側に保持しているエラー情報は捨てることになります。この場合には、エラー情報(Exception)を最初の段階で捨ててしまい、エラーか否かという情報のみを伝搬しても得られる結果は同じです。

前述の例では、new IllegalArgumentException("bad")で生成したExceptionを次の処理ですぐに捨てています。もったいないですね。

以下ではRightProjectionのtoOptionメソッドでEitherをOptionに変換してしまいます。Optionに変換後、withFilterメソッド、mapメソッドとgetOrElseメソッドの合わせ技で値を取得しています。

def f(a: Either[Exception, Int]): String = {
  a.right.toOption.withFilter(_ >= 0).map(_.toString) getOrElse ""
}

Scalaz

ScalazではEitherが右側を成功文脈とする成功/失敗の計算文脈としても動作するので、Either#rightメソッドでRightProjectionを取得しなくてもEitherに対して直接flatMapメソッドを適用することができます。ただし、ScalazでもEither#getOrElseメソッドはないので、rightメソッドでRightProjectionを取得する必要があります。このためScala版と比べてそれほど違いは出てきません。

def f(a: Either[Exception, Int]): String = {
  a.flatMap { b =>
    if (b >= 0) b.toString.right
    else new IllegalArgumentException("bad").left
  }.right getOrElse ""
}

また、Booleanのeitherメソッドを使う方法もあります。

def f(a: Either[Exception, Int]): String = {
  a.flatMap { b =>
    !(b >= 0) either {
      new IllegalArgumentException("bad")
    } or b.toString
  }.right getOrElse ""
}

SclazではflatMapを「>>=」の記号で記述することができます。上の例はそれぞれ以下のようになります。

def f(a: Either[Exception, Int]): String = {
  (a >>= { b =>
    if (b >= 0) b.toString.right
    else new IllegalArgumentException("bad").left
  }).right getOrElse ""
}
def f(a: Either[Exception, Int]): String = {
  (a >>= { b =>
    !(b >= 0) either {
      new IllegalArgumentException("bad")
    } or b.toString
  }).right getOrElse ""
}
Optionに変換

ScalazでもEitherにtoOptionメソッドは追加されないのでrightメソッドで得られるRightProjectionのtoOptionメソッドを用いてOptionに変換します。

def f(a: Either[Exception, Int]): String = {
  a.right.toOption.withFilter(_ >= 0).map(_.toString) | ""
}

Scalazでは、Optionに対して色々な機能拡張を行っているので、Optionに変換すればいろいろな技を使うことが可能になります。ここでは「|」メソッドで値を取り出しています。

また、以下のようにデフォルト値がモノイドの単位元と同じ場合にはorZeroメソッドを使うこともできます。

def f(a: Either[Exception, Int]): String = {
  a.right.toOption.withFilter(_ >= 0).map(_.toString) orZero
}

ノート

前回のmapメソッドと同様に、flatMapメソッドを使う場合も、計算文脈をEitherからOptionに切り替えるテクニックが有力です。

Eitherを使う場合は、計算文脈を成功文脈から失敗文脈に切り替える時にflatMapメソッドを使うしか選択肢はありませんが、Optionにすることで、withFilterメソッドやcollectメソッドなども使えるようになります。

また、ScalazはOptionに対して色々な機能拡張をしているので、さらにプログラミングの選択肢が広がります。今回の例では「|」メソッドやorZeroメソッドで値を取り出しています。

Monadicプログラミングでは、計算文脈の中で文脈を切り替えていく(Eitherの中でLeftとRightを切り替え)のに加え、計算文脈毎、応用に適したものに切り替えていく(EitherをOptionに切り替え)のも重要なテクニックになります。

モナドで実現された階層化複合化された計算文脈を適材適所で切り替えながら処理を進めていくのがMonadicプログラミングの醍醐味と言えるかもしれません。

flatMapと「>>=」と「∗」

Scalazは型クラスをベースにしたMonadicプログラミングを指向したクラスライブラリですが、Scala言語本体にもモナドが実装されています。このため、モナドの基本操作の部分ではScala本体とScalazの機能がバッティングします。

ボク個人のプログラミング方針としては、Scala本体とScalazがバッティングしていて機能がほとんど変わらない場合はScala本体を選ぶようにしています。というのは、Scalazは余分なオブジェクトを相当数生成しながら動作するのでかなり重たいからです。

ただ、これは簡潔に記述できることとのトレードオフでもありますし、Scalazを選んだ以上多少のことは目をつぶって簡潔さに賭けるというのが潔く、現在のハードウェア性能では多くの場合許容範囲と考えられます。

さらに本記事のようなScalazイディオムという意味では、よりScalazらしい書き方を主にした方が趣旨に合います。

そういうこともあって、ボクの書き癖でflatMapを使ってきましたが、Scalazらしく機能がバッティングする場合も「>>=」といったScalaz側のメソッドを主にしていくことにしました。

また、flatMapは「∗」、mapは「∘」という記号(Unicode)で記述する方法もあり、flatMap、「>>=」、「∗」のどれを選ぶのか悩ましいところです。「∗」は「∘」と紛らわしいので、flatMap、「>>=」、「∗」の選択では「>>=」を使うことにします。(Haskellの流儀に則っていて、モナドや型クラス周りの知識のある人には読みやすいというのもあります。)

なお、Eitherに関していうと、Scala標準ではEitherにはflatMapメソッドはないので、Scalazを使っていることを明示する目的で「>>=」を使うという考え方もあります。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

2012年2月15日水曜日

Scala Tips / Either (3) - getOrElse

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

EitherをOption的な成功/失敗文脈で使う方法について考えています。右側が成功文脈、左側が失敗文脈としています。

Eitherの場合も、Optionと同様に値を取り出す処理として以下の2つのコーディングパターンがあります。

  • Either[A, B]からEither[A, C]に変換
  • Eigher[A, B]からCに変換

ここまではEither[A, B]からEither[A, C]への変換についてみてきました。今回はEither[A, B]からCへの変換について考えます。

条件結果演算
Either[A, B]がRight[A, B]CBからCを計算
Either[A, B]がLeft[A, B]Cデフォルト値

Either[A, B]がRight[A, B]の場合は、この値からCを計算しますが、Left[A, B]の場合はあらかじめ用意しているデフォルト値を返します。

以下では、Either[Exception, Int]からStringへの変換を例に考えてみます。Either[Exception, Int]がLeft[Exception]の場合はデフォルト値として空文字列「""」を返すことにします。

(分類の基準)

Java風

if式でEither#isRightを使って右側の値の有無を判定します。デフォルト値の「""」はelse句で指定します。isRightメソッドとgetメソッドが泣き別れになっているのがあまりよい感触ではありません。

def f(a: Either[Exception, Int]): String = {
  if (a.isRight) a.right.get.toString
  else ""
}

Scala風

match式を使うと以下のようになります。こちらの方が綺麗です。

def f(a: Either[Exception, Int]): String = {
  a match {
    case Right(b) => b.toString
    case Left(_) => ""
  }
}

Scala

EitherでMonadic演算をしたい場合にはright(またはleft)メソッドでRightProjection(またはLeftProjection)を取得し、これに対してmapメソッドやflatMapメソッドを適用します。EitherとRightProjection/LeftProjectionの組合せでモナド的な動作をします。

以下ではEitherからrightメソッドでRightProjectionを取り出しmap適用、mapの結果のEitherからrightメソッドでRightProjectionを取り出しgetOrElseで値を取り出す、という形でEither→right→RightProjectionの連鎖で処理を進めています。

最後に、flatMapの結果のEitherからrightメソッドでRightProjectionを取り出しgetOrElseで値を取り出します。

def f(a: Either[Exception, Int]): String = {
  a.right.map(_.toString).right getOrElse ""
}
Optionに変換

最終的に値を取り出す場合、左側に保持しているエラー情報は捨てることになります。この場合には、エラー情報(Exception)を最初の段階で捨ててしまい、エラーか否かという情報のみを伝搬しても得られる結果は同じです。

そこで、最初の段階でRightProjectionのtoOptionメソッドでOptionに変換し、Optionに対してMonadic演算を行うようにします。

以下ではOptionに変換後、mapメソッドとgetOrElseメソッドの合わせ技で値を取得しています。

def f(a: Either[Exception, Int]): String = {
  a.right.toOption.map(_.toString) getOrElse ""
}

Scalaz

ScalazではEitherが右側を成功文脈とする成功/失敗の文脈としても動作するので、Either#rightメソッドでRightProjectionを取得しなくてもEitherに対して直接mapメソッドを適用することができます。ただし、ScalazでもEitherにgetOrElseメソッドはないので、rightメソッドでRightProjectionを取得する必要があります。このためScala版と比べてそれほど違いは出てきません。

def f(a: Either[Exception, Int]): String = {
  a.map(_.toString).right getOrElse ""
}
Optionに変換

ScalazでもEitherにtoOptionメソッドは追加されないのでrightメソッドで得られるRightProjectionのtoOptionメソッドを用いてOptionに変換します。

def f(a: Either[Exception, Int]): String = {
  a.right.toOption.map(_.toString) | ""
}

Scalazでは、Optionに対して色々な機能拡張を行っているので、Optionに変換すればいろいろな技を使うことが可能になります。ここでは「|」メソッドで値を取り出しています。

また、以下のようにデフォルト値がモノイドの単位元と同じ場合にはorZeroメソッドを使うこともできます。

def f(a: Either[Exception, Int]): String = {
  a.right.toOption.map(_.toString) orZero
}

ノート

Eitherは、Optionに比べるとかなり扱いにくいオブジェクトです。Scala標準クラスライブラリでは、あくまでも直和、選択を表現するためのオブジェクトであり、典型的な使い方である成功/失敗文脈を扱うのに便利な機能(右側を特別扱いする関数など)が用意されていません。またEitherそのものはOptionのようなモナドでもありませんし、今の所利用頻度がそれほど多くないためか、その他Eitherを取り回す時に便利なユーティリティ機能が少ないので、Optionのような使いやすさにはなりません。

Scalazでも、多少状況は緩和されますが扱いにくいオブジェクトであることは変わりません。

Eitherの文脈を維持する必要がある場合は仕方ありませんが、今回のようにエラー情報そのものは捨ててもよい場合には、最初の段階で計算文脈をEither(エラー情報を保持する成功/失敗文脈)からOption(エラー情報を保持しない成功/失敗文脈)に切り替えるのが有力なテクニックです。

計算文脈の複雑度が低くなるのでプログラミングが扱うべき事項も小さくなります。また、OptionはScalaでもScalazでも、Eitherに比べると機能が豊富でいろいろな技が使えるので、プログラミングの選択肢が広がります。

エラー情報を保持しつつ、成功/失敗の計算文脈でプログラミングを行う場合は、ScalazのValidationが有力な選択肢になります。ValidationはEitherに続けて取り上げる予定です。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

2012年2月14日火曜日

Scala Tips / Either (2) - flatMap

Eitherを成功/失敗の文脈で使用する方法のイディオムです。

前回はEitherをOptionと同じ成功/失敗の文脈で使用する方法について見てきました。

これは以下の演算になりますが、ちょうど、Optionでいうと「 Option (3) - map 」に相当する処理です。

条件結果
Either[A, B]がRight[A, B]Right[A, C]
Either[A, B]がLeft[A, B]Left[A, C]

今回は以下の処理、アプリケーションの意図で成功の文脈を失敗の文脈に切り替えるためのMonadic演算についてみていきます。Optionでいうと「Option (6) - flatMap 」に相当する処理になります。

条件結果
Either[A, B]のRight[A, B]に有効な値が入っているRight[A, C]
Either[A, B]のRight[A, B]に無効な値が入っているLeft[A, C]
Either[A, B]がLeft[A, B]Left[A, C]

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

以下では、Either[Exception, Int]からEither[Exception, String]への変換を例に考えてみます。ただし、Intは0以上のものが有効という条件を追加します。Either(Right)に入っているIntが0以上の場合、Right[Exception, String]が処理結果となります。一方、0未満の場合は無効となりLeft[Exception, String]が処理結果となります。

Java風

if式でEither#isRightメソッドを使ってLeftとRightの判定をして、処理を切り分ける事ができます。

def f(a: Either[Exception, Int]): Either[Exception, String] = {
  if (a.isRight) {
    if (a.right.get >= 0) {
      Right(a.right.get.toString)
    } else {
      Left(new IllegalArgumentException("less than 0"))
    }
  } else {
    a.asInstanceOf[Either[Exception, String]]
  }
}

Scala風

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

def f(a: Either[Exception, Int]): Either[Exception, String] = {
  a match {
    case Right(b) if (b >= 0) => Right(b.toString)
    case Right(_) => Left(new IllegalArgumentException("less than 0"))
    case Left(b) => Left(b)
  }
}

Scala

Eitherのrightメソッドで得られるRightProjectionがモナドっぽい動きをするので、flatMapメソッドを使ってMonadicプログラミングします。flatMapメソッドでは、Int値が0以上である場合は有効な値なのでStringに変換する演算を行い結果をRightオブジェクトに詰めて返します。一方、Int値が0未満の場合は、無効な値なので成功の文脈から失敗の文脈への切り替えとして、LeftオブジェクトにIllegalArgumentExceptionを詰めて返します。

def f(a: Either[Exception, Int]): Either[Exception, String] = {
  a.right.flatMap { b =>
    if (b >= 0) Right(b.toString)
    else Left(new IllegalArgumentException("less than 0"))
  }
}

Scalaz

Scalazを使うと、Eitherが右側を成功の文脈として動作するモナドに拡張されるので、これを利用したプログラミングが可能です。やはりflatMapメソッドを使います。また、LeftとRightの生成をleftメソッド、rightメソッドで簡潔に記述できるようになります。

def f(a: Either[Exception, Int]): Either[Exception, String] = {
  a.flatMap { b =>
    if (b >= 0) b.toString.right
    else new IllegalArgumentException("less than 0").left
  }
}

ノート

前回は、Eitherのrightメソッドで返ってくるRightProjectionを使えばMonadic演算が可能なことを説明しました。

RightProjectionを使って、Optionで使用したwithFilter, collect, flatMap, for式といった技が駆使できると嬉しいのですが、残念ながらそうはなっていません。以下の理由により、この中で使える/使って便利なのはflatMapのみとなります。

collectメソッドはRightProjectionにそもそも機能がありません。

withFilterメソッドやfilterメソッドはEither[A, B]ではなくOption[Either[A, B]]を返すので、Optionのハンドリングが余分に必要になります。これは、それなりのコードになってしまうので、それよりflatMapメソッドを使ったほうが簡潔です。

for式は内部的にwithFilterメソッドを使っているので、やはり使えません。

以上の理由で、Eitherに対しては他の機能のことは考えずに、文脈の切り替えはflatMapメソッド一本で考えていくのが得策です。

Scalazの場合も事情は同じで(Left/RightProjectionではなく)EitherのflatMapメソッドを使って処理を記述することになります。

追記 (2012-02-14)

ひなたねこさんのツイートでよりScalazらしい書き方が判明したので補足します。

以下はEither[Exception, Int]の生成にBooleanのeitherメソッドを使ったバージョンです。Right(…)、….rightやLeft(…)、….leftが出てこないので、より簡潔でScalazらしいですね。

def f(a: Either[Exception, Int]): Either[Exception, String] = {
  a.flatMap { b =>
    !(b >= 0) either {
      new IllegalArgumentException("less than 0")
    } or b.toString
  }
}
またScala版の記述をleftメソッド、rightメソッドを使うものに更新しました。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

2012年2月13日月曜日

Scala Tips / Either

Eitherは直和(disjoint union)を表現するオブジェクトです。数学的な意味はこちらを参照してください。

直和は、プログラミング的な観点でざっくりというと二つの種類の値のどちらかを選択する(choice)という意味です。

Eitherでは、2つの種類の値のことをそれぞれleftとrightと呼んでいます。以下ではleftの値を「左側」、rightの値を「右側」と表現することにします。

Eitherの典型的な使い方は、関数の成功と失敗を、成功時の返却値、失敗時の返却値と合わせて通知するというものです。EitherのサブクラスはRightとLeftの2つで、成功の場合はRight、失敗の場合はLeftを使う慣習になっています。右側のRightを成功の用途に使うという慣習は、いうまでもなく「Right=正しい」という英語の意味にかけています。

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

今回はOption(3)の課題のEither版を考えてみます。

条件結果
Either[A, B]がRight[A, B]Right[A, C]
Either[A, B]がLeft[A, B]Left[A, C]

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

以下で左側(失敗側)にException、右側(成功側)にIntを持つをEither、つまりEither[Exception, Int]をEither[Exception, String]に変換するプログラムを考えます。

(分類の基準)

Java風

if式でEither#isRightメソッドを使ってLeftとRightの判定をして、処理を切り分ける事ができます。

def f(a: Either[Exception, Int]): Either[Exception, String] = {
  if (a.isRight) {
    Right(a.right.get.toString)
  } else {
    a.asInstanceOf[Either[Exception, String]]
  }
}

Scala風

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

def f(a: Either[Exception, Int]): Either[Exception, String] = {
  a match {
    case Right(b) => Right(b.toString)
    case Left(b) => Left(b)
  }
}

Scala

Eitherのrightメソッドで得られるRightProjectionがモナドっぽい動きをするので、これを使ってMonadicプログラミングします。

def f(a: Either[Exception, Int]): Either[Exception, String] = {
  a.right.map(_.toString)
}

RightProjectionそのものはモナドではなく、EitherとRightProjectionを合わせて一つのモナド、というような動きになります。たとえば、mapメソッドを繋げる場合は以下のようになります。

a.right.map(_.toString).right.map(_.toInt)

Scalaz

Scalazを使うと、Eitherが右画はを成功の文脈として動作するモナドに拡張されるので、これを利用したプログラミングが可能です。

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

ScalazではEitherが、エラー情報付きのOption的な機能を持つオブジェクト/モナドになるわけです。

ノート

ScalaのEitherはモナドではないので、Monadicな演算、つまり計算文脈上で値を操作する演算をするためにはちょっとコツが要ります。

Eitherのleftメソッド、rightメソッドで得られるLeftProjection、RightProjectionがモナド的な動きをします。EitherとLeftProjection、RightProjectionを合わせて一つのモナドという使い方になります。

Eitherでは左側と右側を公平に扱っています。左側を主にMonadic演算をする場合は右側は自動的に引き継がれる、逆に右側を主にMonadic演算をする場合は左側は自動的に生き継がれる、という動きになります。左側が主の場合は、leftメソッドで得られるLeftProjection、右側が主の場合は、rightメソッドで得られるRightProjectionを使うわけです。直和を表現するという趣旨からは妥当な仕様です。

しかし、Eitherの典型的な使い方である成功/失敗の文脈の用途では、右側を成功の文脈とするのが慣習となっているため、Either自身が右側を成功の文脈とするモナドである方が使い勝手が良くなります。

Scalazを使うと、まさにこのEitherの右側を成功の文脈とするモナドの機能が付加されます。

日々のプログラミングでは、Option的な成功/失敗の文脈が、Eitherの主たる用途になるのでScalazの拡張を利用するとよいでしょう。

Scala基本クラスの提供する左側と右側が平等に扱われるMonadic演算も、応用にハマれば面白い使い方ができそうです。

追記 (2012-02-13)

よい表現を思いついたので更新しました。
  • 二つの種類の値のどちらかを取る→二つの種類の値どちらかを選択する(choice)

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

2012年2月12日日曜日

SmartDox 0.2.2

SmartDox 0.2.2をリリースしました。

本バージョンはorg-modeの解析方法を改良しました。

機能

SmartDox 0.2.2では以下のオプションを提供しています。

オプション機能
-html5HTML5生成(試験的)
-html4HTML4生成
-html3HTML3生成
-plainプレインテキスト生成
-pdfPDF生成
-latexLaTeX生成
-bloggerBlogger用のHTML生成

基本的にSmartDox 0.2.1と同じです。org-modeの解析方法を改良しました。

インストール

プログラムの配布は、Scalaで最近注目されているconscriptを使っています。conscriptのインストール方法は以下のページに詳しいです。

Linux, Macであれば、以下のようにすればインストール完了です。

$ curl https://raw.github.com/n8han/conscript/master/setup.sh | sh

conscriptをインストールした後、以下のようにしてSmartDoxをインストールします。

$ cs asami/dox

以下の2つのコマンドがインストールされます。

dox
SmartDoxコマンド
sdoc
SmartDocコマンド(互換用)
依存プロダクト

SmartDoxでは、以下のプロダクトに依存しています。

プロダクト使用する機能
LaTeXPDF生成
Graphviz画像生成
Ditaa画像生成

プロダクトに依存する機能を使わない場合は必要ありません。

LaTeX

platexコマンドとdvipdfmxコマンドが実行可能になっていれば基本的にはOKです。

Mac OS上でmacportsを使ってインストールしたLaTeXで動作確認しています。他の環境の場合、スタイルファイルなどがない可能性があります。

Graphviz

dotコマンドが実行可能になっていればOKです。

Mac OS上でmacportsを使ってインストールしたGraphvizで動作確認しています。

Ditaa

ditaaコマンドが実行可能になっているか、optlocalsharejavaditaa09.jarのJarファイルが存在していればOKです。

Mac OS上でmacportsを使ってDitaaをインストールすると、optlocalsharejavaditaa09.jarに配置されます。このditaa09.jarを決め打ちで使用しています。(いずれパラメタで指定可能にする予定です。)

それ以外の環境では、シェルスクリプトなどでditaaコマンド(インストールされているJarファイルを呼び出す)を作成してください。

使い方

まだマニュアルがないので、文書フォーマットは org-modeを参考にしてください。あまり難しい文法を使わなければ大体大丈夫だと思います。

org-mode形式で作成した文書から以下のようにしてHTMLやPDF、プレインテキストに変換してください。

$ dox -html4 mydoc.dox
$ dox -pdf mydoc.dox
$ dox -plain mydoc.dox

サンプル

SmartDoxでは以下のようなorg-mode文書が扱えます。

#+title: SmartDoxサンプル
#+author: 浅海
#+date: 2012年2月12日

* 文章

これは *SmartDox* の文章です。

- SmartDoxのコンセプトは(org-mode+html5)/2
- HTMLに加えてPDFやプレインテキストを生成することができます。
- GraphvizやDitaaの画像を生成して埋め込むことができます。

* 表

| オプション | 機能                 |
|------------+----------------------|
| -html5     | HTML5生成(試験的)    |
| -html4     | HTML4生成            |
| -html3     | HTML3生成            |
| -plain     | プレインテキスト生成 |
| -pdf       | PDF生成              |
| -latex     | LaTeX生成            |
| -blogger   | Blogger用のHTML生成  |

* 画像

** graphviz

[[http://www.graphviz.org/][Graphviz]] の図を直接書くことができます。

#+begin_dot images/dot_example.png -Tpng
digraph G {
  Hello->World
}
#+end_dot

** ditaa

[[http://ditaa.sourceforge.net/][Ditaa]] の図を直接書くことができます。

#+begin_ditaa images/ditaa_example.png
+--------+   +-------+    +-------+
|        | --+ ditaa +--> |       |
|  Text  |   +-------+    |diagram|
|Document|   |!magic!|    |       |
|     {d}|   |       |    |       |
+---+----+   +-------+    +-------+
    :                         ^
    |       Lots of work      |
    +-------------------------+
#+end_ditaa

** SimpleModeler

SimpleModelerを使ってCSVでクラス図を
書くことができます。

#+begin_sm_csv images/sm_csv_simplemodel.png
#actor
顧客
個人顧客,,,,,顧客
法人顧客,,,,,顧客
#resource
商品,商品名,,商品区分(第1類;第2類;第3類)
#event
購入する,,顧客;商品
#+end_sm_csv
PDF

PDFの生成は以下のようにして行います。PDFの生成時に画像の生成も自動的に行いPDF内に埋め込まれます。

$ dox -plain sample.dox

3ページのPDFが生成されます。2ページ目は以下のように表や図が記述されています。


1ページ目はタイトル、3ページ目はditaaとSimpleModeleの図です。



プレインテキスト

ブラウザでは崩れて見えますが、等幅フォントを使えば表やタイトル下の下線も正しくレイアウトされます。

SmartDoxサンプル
                           ━━━━━━━━

                            2012年2月12日
                                 浅海


目次
──

  1 文章
  2 表
  3 画像
    3.1 graphviz
    3.2 ditaa
    3.3 SimpleModeler


1 文章
───

  これは SmartDox の文章です。

    - SmartDoxのコンセプトは(org-mode+html5)/2
    - HTMLに加えてPDFやプレインテキストを生成することができます。
    - GraphvizやDitaaの画像を生成して埋め込むことができます。


2 表
──

┏━━━━━┯━━━━━━━━━━┓
┃オプション│        機能        ┃
┣━━━━━┿━━━━━━━━━━┫
┃-html5    │HTML5生成(試験的)   ┃
┠─────┼──────────┨
┃-html4    │HTML4生成           ┃
┠─────┼──────────┨
┃-html3    │HTML3生成           ┃
┠─────┼──────────┨
┃-plain    │プレインテキスト生成┃
┠─────┼──────────┨
┃-pdf      │PDF生成             ┃
┠─────┼──────────┨
┃-latex    │LaTeX生成           ┃
┠─────┼──────────┨
┃-blogger  │Blogger用のHTML生成 ┃
┗━━━━━┷━━━━━━━━━━┛


3 画像
───


3.1 graphviz
──────

  Graphviz<http://www.graphviz.org/> の図を直接書くことができます。
  <images/dot_example.png>


3.2 ditaa
─────

  Ditaa<http://ditaa.sourceforge.net/> の図を直接書くことができます。
  <images/ditaa_example.png>


3.3 SimpleModeler
─────────

  SimpleModelerを使ってCSVでクラス図を書くことができます。
  <images/sm_csv_simplemodel.png>

2012年2月10日金曜日

Scala Tips / Option - Index

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をnullの問題を回避するためのコンテナとみるとちょっとした便利機能のように思ってしまいますが、成功/失敗の計算文脈を実現するモナドとしてみると、関数型プログラミングの広大な世界が垣間見えてきます。

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

2012年2月9日木曜日

Scala Tips / Option (11) - Some/None

OptionのサブクラスであるSomeとNoneを生成するイディオムです。

Scala

Some[Int]とNoneは以下のようにして生成します。(Noneは実際にはシングルトンです。)

val a = Some(10)
val a = None

通常はこの生成方法で良いのですが、一つ問題があります。Some(10)の型はOption[Int]ではなくSome[Int]に、Noneの型もOption[Int]ではなくNone[Nothing]になってしまいます。

scala> val a = Some(10)
a: Some[Int] = Some(10)

scala> val a = None
a: None.type = None

このため、型をOption[Int]にするためには以下のように変数定義に記述するか:

val a: Option[Int] = Some(10)
val a: Option[Int] = None

オブジェクト側に型注釈を付ける必要があります。

val a = Some(10): Option[Int]
val a = None: Option[Int]

いずれもちょっと冗長ですね。

Scalaz

Scalazでは、以下の記述方法でSomeとNoneを生成することが可能です。

val a = 10.some
val a = none[Int]

「10.some」のように任意のオブジェクトのsomeメソッド(Scalazが追加)によってOption[Int]型のインスタンスとしてSome[Int]を生成することができます。また、「none[Int]」のように型名を指定することでOption[Int]型のインスタンスとしてNoneを生成することができます。

scala> 1.some
res53: Option[Int] = Some(1)

scala> none[Int]
res54: Option[Int] = None
BooleanをOptionに変換

Scalazでは、Optionを生成する方法としてBooleanのoptinメソッドを使用する方法があります。(Option(6))

def f(cond: Boolean, value: Int): Option[Int] = {
  cond.option(value)
}

条件が真だった場合は指定した値でSomeを、偽だった場合はNoneを作成します。BooleanをOptionに変換する機能ということができます。

ノート

Some(10)10.some の違いは、一つは見た目として関数型プログラミング的に 10.some の方が見やすいというのがあると思います。この問題はテーマとしていずれ取り上げたいと思います。これは、あくまで主観的なものなので、見た目の問題だけならどちらの記法を採るのかというのは好みの問題となります。

ということで、もっと実利的な意味で 10.some が有益なケースというのを知りたいところです。

Scalazの 10.some という記法が具体的に役に立つのは、関数の引数で型情報まで記述する場合です。

典型的な例がfoldLeftメソッドです。foldLeftメソッドはで畳込みの結果を積算する値の型は、引数に指定する値に付帯する形で指定しなければなりません。

List#foldLeftメソッドを使ってList[Int]の内容を積算するプログラムを例に考えます。Int値がすべて0以上の場合は積算を行いますが、一つでも0未満のものが合った場合はエラーとします。積算が成功した場合はSome[Int]を、失敗した場合はNoneを返します。

まず、初期値として普通にSome(0)を指定してみます。

以下の指定はコンパイルエラーになります。初期値として Some(0) とするとSome[Int]型になってしまうので、Option[Int]として演算ができなくなってしまうわけです。

def f(l: List[Int]): Option[Int] = {
  l.foldLeft(Some(0)) { (a, e) =>
    if (e >= 0) a.map(_ + e) else None
  }
}

これは、以下のように型を明記すれば解決しますが、ちょっと冗長ですね。

def f(l: List[Int]): Option[Int] = {
  l.foldLeft(Some(0): Option[Int]) { (a, e) =>
    if (e >= 0) a.map(_ + e) else None
  }
}

Scalazの 0.some は型がOption[Int]になるので、この問題が発生しません。

def f(l: List[Int]): Option[Int] = {
  l.foldLeft(0.some) { (a, e) =>
    if (e >= 0) a.map(_ + e) else none
  }
}
Optionに対するfoldLeftでの畳込み

ついでなのでOption操作の復習も兼ねて、foldLeftでOptionに対する畳込みをいくつか書いてみました。

def f1(l: List[Int]): Option[Int] = {
  l.foldLeft(0.some) { (a, e) =>
    if (e >= 0) a.map(_ + e) else none
  }
}

def f2(l: List[Int]): Option[Int] = {
  l.foldLeft(0.some) { (a, e) =>
    (e >= 0).fold(a.map(_ + e), none)
  }
}

def f3(l: List[Int]): Option[Int] = {
  l.foldLeft(0.some) { (a, e) =>
    for (x <- a if e >= 0) yield x + e
  }
}

def f4(l: List[Int]): Option[Int] = {
  l.foldLeft(0.some) { (a, e) =>
    a.withFilter(_ => e >= 0).map(_ + e)
  }
}

同じ処理でも色々な書き方ができます。もう少し複雑な処理だと、それぞれの書き方との相性が出てくるので、色々ストックしておいて適材適所で選んでいくようにしたいですね。

関数説明参考
f1if式を使った普通の手法 Option(4)
f2Booleanのfoldを使った手法Option(8)
f3for式を使った手法Option(7)
f4withFilterとmapを使った手法Option(4)

追記 (2012-02-12)

xuwei_kさんのご指摘で抜けがあることが分かったので補足です。

Some(10)10.some の比較をして、後者の方がOption[Int]という型になるので、使いやすいという話をしました。この点について補足です。

Scalaの標準ライブラリでは Some(10) とは別に Option(10) というOptionの生成方法を用意していて、この場合はOption[Int]型の Some(10) を生成します。本記事の用途では、Someに関しては Option(10) の方法でも実現可能です。Scalazを使わない場合は、こちらを使うとよいでしょう。

ただし、Noneに関しては、ちょっとややこしくなります。

Option(null) で生成できるものの型がOption[NULL]になってしまいます。

格納するオブジェクトがAnyRefである場合には、Option[String](null)Option(null: String) という記法が可能です。(関連「null」)

scala> Option[String](null)
res32: Option[String] = None

scala> Option(null: String)
res29: Option[String] = None

ただし、Anyの場合は使えません。

scala> Option[Int](null)
<console>:8: error: type mismatch;
 found   : Null(null)
 required: Int
Note that implicit conversions are not applicable because they are ambiguous:
 both method Integer2intNullConflict in class LowPriorityImplicits of type (x: Null)Int
 and method Integer2int in object Predef of type (x: java.lang.Integer)Int
 are possible conversion functions from Null(null) to Int
              Option[Int](null)
                          ^

scala> Option(null: Int)
<console>:8: error: type mismatch;
 found   : Null(null)
 required: Int
Note that implicit conversions are not applicable because they are ambiguous:
 both method Integer2intNullConflict in class LowPriorityImplicits of type (x: Null)Int
 and method Integer2int in object Predef of type (x: java.lang.Integer)Int
 are possible conversion functions from Null(null) to Int
              Option(null: Int)
                     ^

以上のようにNoneの場合は、色々と考えないといけないことがあります。この辺の事情を覚えておいてプログラミング中に使い分けても、特にメリットがあるところではないので、Noneを生成する場合は変数や関数シグネチャの型定義またはオブジェクトに対する型注釈で型を補うという方針にしておくのがよいと思います。

scala> None: Option[Int]
res31: Option[Int] = None

Scalazを使っている場合は、本文中にあったように、10.some , none[Int] の記法を使うようにするのが分かりやすくてよいでしょう。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

2012年2月8日水曜日

Scala Tips / Option (10) - Exception

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

Optionから値を取り出す際に、以下の表の演算を行う方法についてみてきました。

条件結果
Option[A]に有効な値が入っているSome[B]
Option[A]に無効な値が入っているNone
Option[A]がNoneNone

今回は「Option[A]に無効な値が入っている」の判定で例外を使用するケースです。例外を使うと、プログラミングにも少なからぬ影響が出てきます。

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

ここで、StringからIntへの変換ができない事の判定に例外を使用します。

(分類の基準)

Java風

if式でOption#isDefinedを使って値の有無を判定します。

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

引数がNoneの場合とNumberFormatExceptionをキャッチした時がNone、toIntで整数値が得られた場合はSome[Int]が演算結果となります。

def f(a: Option[String]): Option[Int] = {
  if (a.isDefined) {
    try {
      Some(a.get.toInt)
    } catch {
      case e: NumberFormatException => None
    }
  } else None
}

Scala風

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

def f(a: Option[String]): Option[Int] = {
  a match {
    case Some(b) => {
      try {
        Some(a.get.toInt)
      } catch {
        case e: NumberFormatException => None
      }
    }
    case None => None
  }
}

Scala

Optionの処理にmapメソッドを使うのがScala的なコーディングです。ただし、今回のケースでは例外をキャッチする処理を行う必要があります。

def f(a: Option[String]): Option[Int] = {
  try {
    a.map(_.toInt)
  } catch {
    case e: NumberFormatException => None
  }
}

mapメソッドの外側でtry/catchを使って例外をキャッチするのは、ちょっと美しくありません。例外に依存するロジックは部品化の妨げになるので、できるだけ局所化して隠蔽したいところです。

このためには、mapメソッドの中で例外をキャッチしたいわけですが、この場合は例外をキャッチしたときに、成功の文脈から失敗の文脈への切り替えを行う必要があるので、Option(6)で取り上げたOption#flatMapメソッドを使用します。

def f(a: Option[String]): Option[Int] = {
  a.flatMap { b =>
    try {
      Some(b.toInt)
    } catch {
      case e: NumberFormatException => None
    }
  }
}

例外をキャッチしてOptionに変換するのはScalaでは頻出の処理なので、専用の関数がオブジェクトscala.util.control.Exceptionに用意されています。よく使うのがcatching関数とallCatch関数です。

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

import scala.util.control.Exception._    

def f(a: Option[String]): Option[Int] = {
  a.flatMap { b =>
    catching(classOf[NumberFormatException]).opt(b.toInt)
  }
}

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

import scala.util.control.Exception._    

def f(a: Option[String]): Option[Int] = {
  a.flatMap { b =>
    allCatch.opt(b.toInt)
  }
}

Scalaz

Scalaz流のエレガントな書き方はないと思います。

汎用的な方法ではありませんが、今回の例題である文字列を数値に変換する処理に関しては、ScalazのparseIntメソッド(parseFloatその他もあります)が利用できます。

def f(a: Option[String]): Option[Int] = {
  a.flatMap(_.parseInt.toOption)
}

parseIntメソッドは数値への変換の成否をValidationオブジェクトとして通知します。ValidationのtoOptionメソッドでOptionに変換します。

ノート

例外を関数型プログラミングの中でどう扱っていくのかというのは、ちょっと悩むところですが、Java由来のOOPとのハイブリッドであるScalaでは避けて通ることはできません。

例外処理の考え方はいずれイディオムとしてまとめる予定ですが、今の所、できるだけ早い段階でOption, Either, Validationにして関数合成を軸としたMonadic演算のラインに乗せる方向が、よいのではないかと考えています。

そういう意味で、Scala 2.8から入ってきたscala.util.control.Exceptionは重要な機能ということができます。

現段階では、Optionまでしか取り上げていないので、Either, Validationが終わったたところで改めて考えることにします。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

2012年2月7日火曜日

Scala Tips / Option (9)

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

Optionから値を取り出す処理として以下の2つのコーディングパターンを挙げました。

  • Option[A]からOption[B]に変換
  • Option[A]からBに変換

前回は、後者のOption[A]からBへの変換について、基本的なイディオムを紹介しました。

条件結果演算
Option[A]がSome[A]BAからBを計算
Option[A]がNoneBデフォルト値

今回は、Option[A]がNoneの場合だけでなく、Some[A]の値が条件を満たさない場合も、デフォルト値を返すようにします。

条件結果演算
Option[A]に有効な値が入っているBAからBを計算
Option[A]に無効な値が入っているBデフォルト値
Option[A]がNoneBデフォルト値

以下では、Option[Int]からStringへの変換を例に考えてみます。ただし、Intは0以上のものが有効という条件を追加します。Optionに入っているIntが0以上の場合、Stringが処理結果となります。一方、0未満の場合は無効となり空文字列「""」が処理結果となります。またOption[A]がNoneの場合も空文字列「""」が処理結果となります。

Java風

if式でOption#isDefinedを使って値の有無を判定します。isDefinedAtメソッドが一回、getメソッドが二回が泣き別れになってしまいます。

def f(a: Option[Int]): String = {
  if (a.isDefined && a.get >= 0) a.get.toString
  else ""
}

Scala風

match式を使うと以下のようになります。こちらの方が綺麗ですね。

def f(a: Option[Int]): String = {
  a match {
    case Some(b) if (b >= 0) => b.toString
    case _ => ""
  }
}

Scala

Option(4)のwithFilterメソッド、mapメソッドとOption のgetOrElseメソッドの合わせ技で実現できます。

def f(a: Option[Int]): String = {
  a.withFilter(_ >= 0).map(b.toString) getOrElse ""
}

Scalaz

Scalazの場合はOption(7)の技法にOption(4)のwithFilterメソッドまたはfilterメソッドを組合わせて、実現できます。

注意点としては、cataメソッドとfoldメソッドの場合はwithFilterではなくてfilterを使う必要があります。cataメソッドとfoldメソッドはScalazが拡張したメソッドで、ScalaのWithFilterオブジェクトは扱えないからです。

def f(a: Option[Int]): String = {
  a.withFilter(_ >= 0).map(_.toString) | ""
}
def f(a: Option[Int]): String = {
  a.filter(_ >= 0).cata(_.toString, "")
}
def f(a: Option[Int]): String = {
  a.filter(_ >= 0).fold(_.toString, "")
}

def f(a: Option[Int]): String = {
  a.withFilter(_ >= 0).map(_.toString) orZero
}
def f(a: Option[Int]): String = {
  ~a.withFilter(_ >= 0).map(_.toString)
}

ノート

今回はOption(4)のwithFilterメソッドを組み合わせてみましたが、Option(5)のcollectメソッド、Option(6)のflatMapメソッドでも同じように実現できます。

要するに、Option上で(つまり成功失敗の計算文脈上で)計算を続け、最後にcataメソッドなどでOptionから値を取り出すというメカニズムです。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

2012年2月6日月曜日

Scala Tips / Option (8)

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

Optionから値を取り出す処理として以下の2つのコーディングパターンを挙げました。

  • Option[A]からOption[B]に変換
  • Option[A]からBに変換

ここまではOption[A]からOption[B]への変換についてみてきました。今回はOption[A]からBへの変換について考えます。

条件結果演算
Option[A]がSome[A]BAからBを計算
Option[A]がNoneBデフォルト値

Option[A]に有効な値が入っている場合は、この値からBを計算しますが、それ以外の場合はあらかじめ用意しているデフォルト値を返します。

以下では、Option[Int]からStringへの変換を例に考えてみます。Option[A]がNoneの場合はデフォルト値として空文字列「""」を返すことにします。

Java風

if式でOption#isDefinedを使って値の有無を判定します。デフォルト値の「""」はelse句で指定します。

def f(a: Option[Int]): String = {
  if (a.isDefined) a.get.toString
  else ""
}

Scala風

match式を使うと以下のようになります。こちらの方が綺麗ですね。

def f(a: Option[Int]): String = {
  a match {
    case Some(b) => b.toString
    case None => ""
  }
}

Scala

Option(3)のmapメソッドとOption のgetOrElseメソッドの合わせ技で実現できます。

def f(a: Option[Int]): String = {
  a.map(_.toString) getOrElse ""
}

Scalaz

Scalazも、Option(3)のmapメソッドとOption の「|」メソッドの合わせ技で実現できます。

def f(a: Option[Int]): String = {
  a.map(_.toString) | ""
}

Scalazでは、今回の用途にぴったりのメソッドOption#cataとOption#foldが用意されています。以下のように使用します。

def f(a: Option[Int]): String = {
  a.cata(_.toString, "")
}
def f(a: Option[Int]): String = {
  a.fold(_.toString, "")
}

foldメソッドはcataメソッド(cataはcatamorphismの略)に分かりやすい名前をつけたもので、全く同じ動作をします。好きな方を使用すればよいでしょう。

以上が汎用的な方法ですが、条件が合えば以下のようにorZeroメソッドや「~」メソッドを使う方法も適用できます。

def f(a: Option[Int]): String = {
  a.map(_.toString) orZero
}
def f(a: Option[Int]): String = {
  ~a.map(_.toString)
}

StringやIntといった型では、Scalazが初期値を持っています。この初期値を単位元と呼びます。デフォルト値がScalazが持っている初期値、すなわち単位元でよい場合は、これを利用するとデフォルト値の指定を省略することができます。今回使用している問題は、デフォルト値が「""」ですが、これはStringの単位元と同じなので、この条件に当てはまります。

このため、OptionがNoneだった場合にデフォルト値として単位元を使うorZeroメソッドと単項演算子の「~」メソッドを使用することができるわけです。

orZeroメソッドと「~」メソッドは好みの方を使えばよいでしょう。orZeroメソッドを使うとちょっとごっつい感じですが、「~」メソッドは簡潔に記述できます。ただ、「~」は単項演算子で、見落としてしまいがちなのと、普通の単項演算子とはちょっと違う感触の使い方なので、プログラムの可読性が必ずしも高くないかもしれません。

ノート

Scalazはモノイドという性質を扱うための型クラスMonoidを用意しています。モノイドの数学的な定義はこちら(Wikipedia)をご覧ください。

数学のモノイドはとても難しそうですが、Scalazの型クラスMonoidが適用されるオブジェクトはざっくりと以下の性質を持つと理解しておけばよいでしょう。

単位元はざっくりいうと a+ee+a の二項演算を行った時に、 a+e=ae+a=a となるeの事です。具体的には、 1+0=10+1=1 における0、 "Hello"+""="Hello"""+"Hello" における「""」です。つまりIntの単位元は0、Stringの単位元は「""」となります。

前述のorZeroメソッドや「~」メソッドは、モノイドの性質である単位元を使用しています。関数fは演算結果がStringでデフォルト値が「""」となる演算を定義しているわけですが、このデフォルト値「""」が、たまたまStringの単位元と同じなので、モノイドの性質を利用しているorZeroメソッドや「~」メソッドを使うことができるわけです。

Scalazでの代表的なMonoidのオブジェクトは、Int、String、Listといったものです。加算的な演算を持っているオブジェクトはだいたいMonoidとして操作できるようになっています。

Int、String、Listといったオブジェクトの共通の性質としてMonoidを定義することで、Monoidを対象にしたロジックを組めば、Int、String、Listといったオブジェクト指向的には全く異なったオブジェクトに同じロジックを適用できるようになります。たとえば、ScalazのValidationはこのモノイドをうまく利用しています。

今回のイディオムでは、Monoidの性質のごく一部を使っているだけでしたが、MonoidはScalazプログラミングでは頻出の極めて重要な型クラスなので少し詳しく説明してみました。

最後に、Monoidの動作をREPLで試してみたものを以下に示します。

単位元はmzero関数で取得できます。

scala> mzero[Int]
res4: Int = 0

scala> mzero[String]
res5: String = ""

scala> mzero[List[Int]]
res7: List[Int] = List()

Monoidの二項演算は|+|メソッドまたは⊹メソッドです。(後者はUnicodeの数学記号になっています。)

scala> 1 |+| 1
res8: Int = 2

scala> 1 |+| mzero[Int]
res9: Int = 1

scala> mzero[Int] |+| 1
res10: Int = 1

scala> "Hello" |+| "World"
res15: java.lang.String = HelloWorld

scala> "Hello" |+| mzero[String]
res16: java.lang.String = Hello

scala> mzero[String] |+| "World"
res17: String = World

scala> List(1, 2, 3) |+| List(4, 5, 6)
res11: List[Int] = List(1, 2, 3, 4, 5, 6)

scala> List(1, 2, 3) |+| mzero[List[Int]]
res13: List[Int] = List(1, 2, 3)

scala> mzero[List[Int]] |+| List(1, 2, 3)
res14: List[Int] = List(1, 2, 3)

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

追加

一部、次回の記事が混入していたので、2月6日8時45分頃に内容を更新しました。

2012年2月3日金曜日

Scala Tips / Option (7)

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

ここまで以下の演算と:

条件結果
Option[A]がSome[A]Some[B]
Option[A]がNoneNone

以下の演算についてみてきました。

条件結果
Option[A]に有効な値が入っているSome[B]
Option[A]に無効な値が入っているNone
Option[A]がNoneNone

前者は、成功の文脈と失敗の文脈の切り替えが発生しない演算、後者は、成功の文脈と失敗の文脈の切り替えが発生する演算です。

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

Some[A]→Some[B]

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

条件結果
Option[A]がSome[A]Some[B]
Option[A]がNoneNone

Option[Int]からOption[String]へ変換は以下のようになります。

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

Option(3)で使用したmapメソッドと同様に、Option[A]がNoneだった場合の処理を書く必要がありません。

Some[A]→None

次はOption(4)で取り上げた成功の文脈と失敗の文脈の切り替えが発生する演算です。以下の表に示す演算になります。

条件結果
Option[A]に有効な値が入っているSome[B]
Option[A]に無効な値が入っているNone
Option[A]がNoneNone

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

def f(a: Option[Int]): Option[String] = {
  for (b <- a if b >= 0) yield b.toString
}

for式内のif句で、Some[A]をNoneに切り替える条件を指定することができます。

ノート

Javaのfor文は構造化プログラミングにおける繰り返しを記述するための文でしたが、Scalaのfor式はモナドによる演算の文法糖衣となっています。

たとえば、以下のプログラムは、一見普通のfor文に見えますが、実際は0オブジェクト(Int型)のuntilメソッドが返すRangeオブジェクトがモナドで、このRangeオブジェクトに対するモナドの演算を行っています。

for (i <- 0 until 10) {
  println(i)
}

モナドに対する演算を伝統的なfor文に見せているのがScalaの芸の細かいところで、Java言語などから移行する場合の敷居を低くしています。普通にプログラミングしていても自然にMonadicプログラミングをしているということになるわけで、使い方に慣れたところで徐々にMonadicプログラミング的な作法を取り入れていけばよいようになっています。

for式には、上記の(yield句なしの)for式と本文中で用いているyield句ありのfor式の2種類があります。

yield句ありのfor式の場合は、内部的に今までOption操作で使ってきた、mapメソッド、flatMapメソッド、withFilterメソッドを使って処理を行っています。つまり、完全な文法糖衣ということです。(yield句なしのfor式ではforeachメソッドを使います。)

今回取り上げているOptionから値を取り出すといった小さな処理では、mapメソッドなどを直接使ってもfor式を使っても効果はそれほど変わりません。好みの方を使うとよいでしょう。

ただし、Option(6)のようにflatMapメソッドを使って細かい操作をしたい場合や、Option(5)のようにcollectメソッドのようなfor式がカバーしていないメソッドを使いたい場合は、for式は諦めることになります。

逆に本格的なMonadicプログラミングを行う場合、for式を使うと見通しがよくなるケースがあるので、こういう場合はfor式を積極的に使っていきたいところです。

このあたりの選択のポイントについてもテーマとして考えていきたいと思います。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

2012年2月2日木曜日

Scala Tips / Option (6)

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

以下の表が示す演算について考えています。

条件結果
Option[A]に有効な値が入っているSome[B]
Option[A]に無効な値が入っているNone
Option[A]がNoneNone

withFilterバージョン、collectバージョンに続いて、flatMapバージョンです。

引き続きIntは0以上のものが有効という条件付きのOption[Int]からOption[String]へ変換を例に考えてみます。

(分類の基準)

Java風

Option(4)と同じです。

Scala風

Option(4)と同じです。

Scala

Option[Int]のflatMapメソッドは、Option[Int]の格納する値であるIntを引数に取り、この場合はOption[String]を返す関数を実行します。Option(3)で取り上げた、mapメソッドの場合はIntを引数に取りStringを返す関数を実行し、その結果をmapメソッド側でOptionに詰めなおしますが、flatMapの場合は、StringをOptionに詰め直す作業もアプリケーション側の関数で行います。

def f(a: Option[Int]): Option[String] = {
  a.flatMap { b =>
    if (b >= 0) Some(b.toString)
    else None
  }
}

Scalaz

Scalazの場合もflatMapメソッドを使いますが、flatMapの中のロジックをより簡潔に記述することができます。

def f(a: Option[Int]): Option[String] = {
  a.flatMap(b => (b >= 0).option(b.toString))
}

Scalazでは、Booleanにoptionメソッドを拡張しています。optionメソッドでは、Booleanが真の場合、指定した関数が実行され、その結果をSomeに詰めたものが返されます。一方、偽の場合はNoneが返されます。

(b >= 0).option(b.toString)は、bの値が0以上の場合にbをStringにしたものをSome[String]に詰めて返し、そうでない場合はNoneを返します。

Scalazでは、このように式を簡潔に記述できる便利なメソッドが多数追加されています。

ノート

今回の用途では、Optionのfilter/withFilterメソッドやcollectメソッドで十分に要件を満たせるので、イディオムとしてはこの2つだけもよかったのですが、プリケーションの意志で成功の文脈を失敗の文脈に切り替えるということを、もっと直接的な形で行う手法をマスターしておかないと応用が効かないので、flatMapメソッドも取り上げました。

以下の表の演算において:

条件結果
Option[A]に有効な値が入っているSome[B]
Option[A]に無効な値が入っているNone
Option[A]がNoneNone

「Option[A]に無効な値が入っている」部分が、アプリケーションの意志で成功の文脈を失敗の文脈に切り替えるところになります。

filter/withFilterメソッドやcollectメソッドは自動的にこの切り替えを行ってくれるわけですが、それぞれのメソッドの機能ははっきり決まっていて、提供された機能以外の用途に使うのは得策ではありません。

そういった汎用目的で成功の文脈を失敗の文脈に切り替える処理を行うのがflatMapメソッドです。

mapメソッドを使うと、成功の文脈における演算を記述することができましたが、成功の文脈を失敗の文脈に切り替えることはできませんでした。flatMapメソッドは、この切り替えを記述するためのメソッドというわけです。

モナドは計算文脈をカプセル化して扱う技術と考えることができます。(モナドの一種であるOptionは成功/失敗の文脈をカプセル化していました。)この計算文脈の操作に色々な手法が存在するわけですが、その中軸となるのがflatMapメソッドです。

flatMapメソッドを使いこなせるようになるとScalaプログラミングの幅がぐんと広がります。

filter, collect, flatMapの使い分け

前回はfilterメソッドとcollectメソッドの使い分けについて説明しましたが、これにflatMapが加わりました。

filterメソッドやcollectメソッドは、使い方の形が決まっているので、これにぴったりハマるケースはfilterメソッドやcollectメソッドを使えばよいでしょう。

あまりぴったりはまらないケース、判定ロジックと生成ロジックが入り乱れているような場合に、flatMapメソッドを使うことになります。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3