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

0 件のコメント:

コメントを投稿