ジェネリシティ 

大局的に見ると、関数型プログラミングは色々なものの抽象化だと考えることができる。 Jeremy Gibbons さんの 2006年の本 Datatype-Generic Programming を流し読みしていると、まとめ的なものが見つかった。

以下は拙訳。

ジェネリック・プログラミングとは、安全性を犠牲にせずにプログラミング言語をより柔軟にすることだ。

値によるジェネリシティ 

全てのプログラマが最初に習うことの一つで、最も重要なテクニックは値をパラメータ化することだ。

scala> def triangle4: Unit = {
         println("*")
         println("**")
         println("***")
         println("****")
       }
triangle4: Unit

4 を抽象化して、パラメータとして追い出すことができる:

scala> def triangle(side: Int): Unit = {
         (1 to side) foreach { row =>
           (1 to row) foreach { col =>
             println("*")
           }
         }
       }
triangle: (side: Int)Unit

型によるジェネリシティ 

List[A] は、要素の型という別の型によってパラメータ化されている 多相的なデータ型 (polymorphic datatype) だ。 これはパラメトリックな多相性 (parametric polymorphism) を可能とする。

scala> def head[A](xs: List[A]): A = xs(0)
head: [A](xs: List[A])A

上の関数は全てのプロパー型に対して動作する。

関数によるジェネリシティ 

高階なプログラムは別のプログラムによりパラメータ化されている。

例えば、foldLeft を使って 2つのリストの追加である append を書くことができる:

scala> def append[A](list: List[A], ys: List[A]): List[A] =
         list.foldLeft(ys) { (acc, x) => x :: acc }
append: [A](list: List[A], ys: List[A])List[A]

scala> append(List(1, 2, 3), List(4, 5, 6))
res0: List[Int] = List(3, 2, 1, 4, 5, 6)

数を足し合わせるのにも使うことができる:

scala> def sum(list: List[Int]): Int =
        list.foldLeft(0) { _ + _ }
sum: (list: List[Int])Int

構造によるジェネリシティ 

Scala Collection のようなコレクション・ライブラリによって体現化される「ジェネリック・プログラミング」がこれだ。 C++ の Standard Template Library の場合は、パラメトリックなデータ型はコンテナとよばれ、 input iterator や forward iterator といったイテレータによって様々な抽象化が提供される。

型クラスという概念もここに当てはまる。

scala> :paste
// Entering paste mode (ctrl-D to finish)
trait Read[A] {
  def reads(s: String): Option[A]
}
object Read extends ReadInstances {
  def read[A](f: String => Option[A]): Read[A] = new Read[A] {
    def reads(s: String): Option[A] = f(s)
  }
  def apply[A: Read]: Read[A] = implicitly[Read[A]]
}
trait ReadInstances {
  implicit val stringRead: Read[String] =
    Read.read[String] { Some(_) }
  implicit val intRead: Read[Int] =
    Read.read[Int] { s =>
      try {
        Some(s.toInt)
      } catch {
        case e: NumberFormatException => None
      }
    }
}

// Exiting paste mode, now interpreting.

defined trait Read
defined object Read
defined trait ReadInstances

scala> Read[Int].reads("1")
res1: Option[Int] = Some(1)

型クラスは、型クラス・コントラクトとよばれる型が満たさなければいけない要請を表す。 また、型クラスのインスタンスを定義することで、それらの要請を提供する型を列挙することができる。 Read[A] における A は全称的 (universal) ではないため、 これはアドホック多相性を可能とする。

性質によるジェネリシティ 

Scala Collection ライブラリの中では、型が列挙する演算よりも込み入った概念が約束されていることがある。

演算のシグネチャの他にも、 これらの演算が満たす法則や、演算の計算量や空間量に対する漸近的複雑度など、機能以外の特性などがある。

法則を持つ型クラスもここに当てはまる。例えば、Monoid[A] にはモノイド則がある。 それぞれのインスタンスに対して、これらの法則を満たしているかプロパティ・ベース・テストのツールなどを使って検証する必要がある。

ステージによるジェネリシティ 

様々な種類のメタプログラミングは、別のプログラムを構築したり操作するプログラムの開発だと考えることができる。 これにはコード生成やマクロも含む。

形によるジェネリシティ 

ここに多相的なデータ型である二分木があるとする:

scala> :paste
// Entering paste mode (ctrl-D to finish)
sealed trait Btree[A]
object Btree {
  case class Tip[A](a: A) extends Btree[A]
  case class Bin[A](left: Btree[A], right: Btree[A]) extends Btree[A]
}

// Exiting paste mode, now interpreting.

defined trait Btree
defined object Btree

次に、似たようなプログラムを抽象化するために foldB を書いてみる:

scala> def foldB[A, B](tree: Btree[A], b: (B, B) => B)(t: A => B): B =
         tree match {
           case Btree.Tip(a)      => t(a)
           case Btree.Bin(xs, ys) => b(foldB(xs, b)(t), foldB(ys, b)(t))
         }
foldB: [A, B](tree: Btree[A], b: (B, B) => B)(t: A => B)B

次の目標は foldBfoldLeft を抽象化することだ。

これらの 2つの畳み込み演算で異なるのは、それらが作用するデータ型の (shape) であって、 それがプログラムそのものの形を変えている。 ここで求めれるパラメータ化の対象はこの形、つまりデータ型やそれらの (ListTree) といったコンストラクタをパラメータ化する必要がある。 これをデータ型ジェネリシティ (datatype genericity) とよぶ。

例えば、fold は以下のように表現できるらしい。

scala> import cats._, cats.data._, cats.implicits._, cats.functor.Bifunctor
import cats._
import cats.data._
import cats.implicits._
import cats.functor.Bifunctor

scala> :paste
// Entering paste mode (ctrl-D to finish)
trait Fix[F[_,_], A]
def cata[S[_,_]: Bifunctor, A, B](t: Fix[S, A])(f: S[A, B] => B): B = ???

// Exiting paste mode, now interpreting.

defined trait Fix
cata: [S[_, _], A, B](t: Fix[S,A])(f: S[A,B] => B)(implicit evidence$1: cats.functor.Bifunctor[S])B

上の例では、S はデータ型の形を表す。 形を抽象化することで、パラメトリックにデータ型ジェネリックなプログラムを書くことができる。 これについては後ほど。

その振る舞いにおいて何らかの方法で形を利用するプログラムはアドホックなデータ型ジェネリックとよぶ。 pretty printer やシリアライザが典型的な例だ。

この例に当てはまりそうなのは Scala Pickling だ。 Pickling はよくある型には予め pickler を提供して、 マクロを使って異なる形に対して pickler のインスタンスを導出している。

この方法のデータ型ジェネリシティは polytypism構造的多相性typecase など様々な名前でよばれ、 Generic Haskell チームが「ジェネリック・プログラミング」と言うときもこの意味だ。….

我々は、パラメトリックなデータ型ジェネリシティこそが「最高基準」であると考え、 講義ノートでも今後は可能な限りパラメトリックなデータ型ジェネリシティに焦点を置く。

Scala 界隈だと、shapeless が形の抽象化に焦点を置いているだろう。

Contents