テストステ論

高テス協会会長が, テストステロンに関する情報をお届けします.

(scala report) Applyのマコーレー・カルキン演算子

Scalazは, 以下のような演算子をApplyで定義している: (m1 |@| m2 |@| m3) f. このような, ちょっとかっこいい演算子はsyntax/というディレクトリ以下に実装されている.

これはマコーレー・カルキン演算子と呼ばれる. 実装が面白いので紹介する.

f:id:akiradeveloper529:20150624093135j:plain

= |@|

マコーレーカルキン演算子でN個の値をつなぐと, applyNのことであると書いてある. 例えば, f1 |@| f2 |@| f3は, 次に関数を受けて, apply3相当のことをする.

  /**
   * DSL for constructing Applicative expressions.
   *
   * `(f1 |@| f2 |@| ... |@| fn)((v1, v2, ... vn) => ...)` is an alternative to `Apply[F].applyN(f1, f2, ..., fn)((v1, v2, ... vn) => ...)`
   *
   * `(f1 |@| f2 |@| ... |@| fn).tupled` is an alternative to `Apply[F].applyN(f1, f2, ..., fn)(TupleN.apply _)`
   *
   * Warning: each call to `|@|` leads to an allocation of wrapper object. For performance sensitive code, consider using
   *          [[scalaz.Apply]]`#applyN` directly.
   */
  final def |@|[B](fb: F[B]) = new ApplicativeBuilder[F, A, B] {
    val a: F[A] = self
    val b: F[B] = fb
  }

apply3は以下:

  def apply3[A, B, C, D](fa: => F[A], fb: => F[B], fc: => F[C])(f: (A, B, C) => D): F[D] =
    apply2(tuple2(fa, fb), fc)((ab, c) => f(ab._1, ab._2, c))

ApplicativeBuilderというのが気になる. 3つの値を|@|でつなぐ時, 以下のようなことが起こる.

  1. f1 |@| f2 によってApplicativeBuilderが作られる(これはApplicativeBuilder2と言ってもよい). ApplicativeBuilderはapplyを持っていて, f: (A, B) => Cを受け取ると, これを適用する.
  2. さらに|@| f3が続くと, ApplicativeBuilder3が作られて, これも同様.

ようするに, 今まで|@|で結合した値の個数分のapplyNを発動させるような仕組みである. でも今までマコーレーが"結合した"女の数となると20では足りないね:-)

以下はapplyのコード:

final class ApplicativeBuilder[M[_], A, B](a: M[A], b: M[B]) {
  def apply[C](f: (A, B) => C)(implicit t: Functor[M], ap: Apply[M]): M[C] = ap(t.fmap(a, f.curried), b)

  final class ApplicativeBuilder3[C](c: M[C]) {
    def apply[D](f: (A, B, C) => D)(implicit t: Functor[M], ap: Apply[M]): M[D] = ap(ap(t.fmap(a, f.curried), b), c)

ap(t.fmap(a. f.curried), b)というのは, Haskellでいうと, f <$> a <*> bのことである. なので, apply2とかを使う方が綺麗でいいと思うのだが, なぜそうなっていないのかは不明. Scala関数型プログラミングをしようとすると, このcurriedというのがかなりばら撒かれるか, あるいはcurriedしなくても済むように, よくあるケースに対してライブラリが努力するという方向になる(Scalazはたぶんそうしている). 言語の方で自動的にcurried化するようにがんばって欲しいのだが, 無理なんだろうか?

(追記)
ApplicativeBuilderは, 古いソースを参照していたようです. 新しいソースではちゃんと直ってる. (xuwei_kさんが教えてくれた)

private[scalaz] trait ApplicativeBuilder[M[_], A, B] {
  val a: M[A]
  val b: M[B]

  def apply[C](f: (A, B) => C)(implicit ap: Apply[M]): M[C] = ap.apply2(a, b)(f)