テストステ論

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

(scala report) ScalazのPimp My Libraryパターン

Scalazは, Haskellのような関数型プログラミングScalaでやろうというライブラリである. Scalaの勉強としても有用であるため, ざくっと眺めてみた. 私は学生時代にJavaを書いていた時は, Apache CommonsやGoogle Collectionsなどを読んで学んだが, Scalaにとっての学ぶ対象はScalazなのだろうと思っている. 我々はよりScalaらしいコードを書かなければならない. Scalaは良い.

2つのことについてまとめる.

  1. どうやって型クラスを表現しているのか
  2. 流れるインターフェイスにするために何をしているか

1 どうやって型クラスを表現しているか

型クラスの定義自体は, scalazの下にある. 例えばMonoid.scalaはscalaz/Monoid.scalaに定義されている. 定義は以下:

trait Monoid[F] extends Semigroup[F] { self =>
  ////
  /** The identity element for `append`. */
  def zero: F
  ...

これは, MonoidたるFに関する性質を定義しており, Haskellでのclass Monoid a where mempty ...に相当する.

ここに驚きがある. 今まで, オブジェクト指向型言語Javaでは, こういう形はジェネリックと教えられて, 京大のOCaml先生が理論に関わったと聞いて関係ないのに誇らしくなって, List[T]のTはコンテナの中身を表す型だと教わったはずである. TがListであるなどとは考えたことがない. ここでかなり混乱するわけであるが, JavaがList[T]のサブクラスとしてArrayListなりLinkedListなりを作って実装を頑張って隠蔽しようとしているのとは逆に, 型クラスはFというさらけ出されたデータ型に対する演算を定義するものだから全く考え方が違うのである. ここに, 統一された何かを垣間見るのであるが, 私の脳に届いた時にはただただ「素晴らシィ. メルシィ」となってしまう.

実際に, Scalazがこれらの具象を実装する時は, 無名クラスとしてインスタンス化する.

  implicit val intInstance: Monoid[Int] with Enum[Int] with Show[Int] = new Monoid[Int] with Enum[Int] with Show[Int] {
    override def shows(f: Int) = f.toString

    def append(f1: Int, f2: => Int) = f1 + f2

    def zero: Int = 0

2 流れるインターフェイスにするために何をしているか

ここまで書いた時点で少なくとも, implicitly[Monoid[Int]].appendのように, Monoidの力を使うことは出来る(Monoid.intInstanceに直接アクセスする必要はない). さらに, 以下のコンパニオンオブジェクトの力を借りれば,

object Monoid {
  @inline def apply[F](implicit F: Monoid[F]): Monoid[F] = F

Monoid[Int].appendのように書くことも出来そうである. しかしたぶん, それがScalaの限界であろう(嘘くさいので一番下に私のアイデアを示す). これはMonadのように演算をずっと続けていくような場合, 値の型が散らばることになる. おそらくこれは, Scala型推論の制約であろうと推察した.

従って一度, オブジェクト指向の流れるインターフェイスの中に入れてしまうしかない. これは, 1.mappend(mzero).mappend(2)のように書けるようになるということである(実験してはいない).

そのために, ScalazはPimp My Libraryパターンというものを使っている. これは. 2.10で導入されたimpilcit classのワークアラウンドとしてずっと使われてきたものであり, implicit defでRichIntに格上げしてメソッドを後付するという例で教わることが多いが, Scalazでも使われている(古いScalaでも動くようにするためだろう).

trait ToMonoidOps extends ToSemigroupOps {
  implicit def ToMonoidOps[F](v: F)(implicit F0: Monoid[F]) =
    new MonoidOps[F](v)

  ////

  def mzero[F](implicit F: Monoid[F]): F = F.zero

trait ToSemigroupOps  {
  implicit def ToSemigroupOps[F](v: F)(implicit F0: Semigroup[F]) =
    new SemigroupOps[F](v)

  ////
  ////
}

final class SemigroupOps[F] private[syntax](val self: F)(implicit val F: Semigroup[F]) extends Ops[F] {
  ////
  final def |+|(other: => F): F = F.append(self, other)
  final def mappend(other: => F): F = F.append(self, other)
  final def ⊹(other: => F): F = F.append(self, other)
  ////
}
  1. mappendがメソッド呼び出しされる
  2. mappendを持っているものがSemigroupOpsしかないのでそれに暗黙変換しましょう(Pimp My Libraryパターン)

という動作により, 具体的な型を推論に任せることが出来る.

あるはずなのに見つからなかったもの

私は, (やや簡略的に書くが)以下のようなものがあれば, Monoid.append(1, 2)と書けるようになると思うのだが, これは上記したapplyがあれば, 続くappendの引数で推論して, intInstanceをimpilcitにとってこれるという仕組みだろうか?ここでのyes/noも意味があるが, コンパニオンオブジェクトのことがちゃんとわかっていないということだと思うので, 課題とする.

object Monoid {
  def append[F](a: F, b: F)(implicit F: Monoid[F]) = F.append(a, b)
}