2008年10月27日月曜日

JavaEE勉強会(DDD読書会)に行ってきた

初回から出てるんだけど、だんだん人が減ってきた。今20人弱くらいかな。初回は40人近くいて、部屋もぎゅうぎゅうだったことを考えると、これくらいがちょうど良くはある。ただ、これ以上減ると和訳の担当がすごい勢いでまわってくるようになるからこれ以上は減らないといいなぁ。

今回は、ドメイン設計を構成する要素である、Serviceについてと、Domain Modelのパラダイムについて、そして最後にExampleによるDDDの解説について。本で言うと、Chapter6~7。

Domain Modelのパラダイムについてのセクションでは、自分の隠しテーマである、
  • いわゆる「入れポン出しポン」はある種のDDDか?
  • 「入れポン出しポン」システムの対象業務は、より深いモデル化は不要なのか?
  • 問題領域のモデルと解決領域のモデルはどちらがPrimaryか?
このあたりが絡んできていたので、余計なことを話しすぎてしまった。koichikさん、和智さんとのやり取りが楽しいっす。でもきっと、いやがってる人いるんだろうなぁ…自重しよう。

これまでの書籍の要約は、以下のWikiにまとまっています。議事録もあるので、今までの議論を振り返ることもできます。ということで、途中からでも参加可能です。

まとめページ

参加は申込はこちらから。

コミュニティサイト

2008年10月17日金曜日

Scalaのリスト-高階操作-

リスト操作でよくあるパターンは、以下のようなものらしい。
  1. リストのすべての要素を何かしらの方法で変換する
  2. 何かしらの基準を満たす要素を取り出す
  3. 何かの演算子を使ってリスト要素を結合する
関数型言語では、こういうのは高階関数を使って実現する。

Map
リストのすべての要素を何かしらの方法で変換する操作は、Mapと言う。MapReduceのMapと同じだよね、たぶん。例えば、DoubleのListのすべての要素に、何かしらの数を掛け合わせたい場合を考えてみる。
def scaleList(xs: List[Double], factor: Double): List[Double] = xs match {
  case Nil => xs
  case x :: xs1 => x * factor :: scaleList(xs1, factor)
}
通常ならこう書くが、共通部分を抽出すればmap関数として定義できる。これは実際にListのメソッドとして定義されている。
abstract class List[A] { ...
  def map[B](f: A => B): List[B] = this match {
  case Nil => this
  case x :: xs => f(x) :: xs.map(f)
}
このmapメソッドを使って先ほどの関数を書き直すと、こんな風になる。簡単。
def scaleList(xs: List[Double], factor: Double) = xs map (x => x * factor)

Filter
何かしらの基準を満たすList要素を取り出す操作は、Filterと言う。例えば、IntのListから0より大きな要素だけ取り出す関数は、以下のように書ける。
def posElems(xs: List[Int]): List[Int] = xs match {
  case Nil => xs
  case x :: xs1 => if (x > 0) x :: posElems(xs1) else posElems(xs1)
}
これらFilter操作の共通的な要素を抜き出すと、このようになる。これも、Listのメソッド。
def filter(p: A => Boolean): List[A] = this match {
  case Nil => this
  case x :: xs => if (p(x)) x :: xs.filter(p) else xs.filter(p)
}
filterメソッドを使えば、postElemsは以下のように書ける。
def posElems(xs: List[Int]): List[Int] = xs filter (x => x > 0)
似たようなものとして、forallとexistsがある。
def forall(p: A => Boolean): Boolean = isEmpty || (p(head) && (tail forall p))
def exists(p: A => Boolean): Boolean = !isEmpty && (p(head) || (tail exists p))
素数かどうか判定するプログラムとか、かなり素敵。
package scala
object List { ...
  def range(from: Int, end: Int): List[Int] = if (from >= end) Nil else from :: range(from + 1, end)

def isPrime(n: Int) = List.range(2, n) forall (x => n % x != 0)

Fold/Reduce
これは、リスト内のすべての要素を何かしらの演算で結合するというもの。例えば、リスト要素をすべて足し合わせるとか、すべて掛け合わせるとか。これは、以下のように書くことができる。
def sum(xs: List[Int]): Int = xs match {
  case Nil => 0
  case y :: ys => y + sum(ys)
}
def product(xs: List[Int]): Int = xs match {
  case Nil => 1
  case y :: ys => y * product(ys)
}
こういう操作は、ListのreduceLeftメソッドがうまくやれる。

List(x1, ..., xn).reduceLeft(op) = (...(x1 op x2) op ... ) op xn

上記の関数をreduceLeftを使って書き換えると、こうなる。

def sum(xs: List[Int]) = (0 :: xs) reduceLeft {(x, y) => x + y}
def product(xs: List[Int]) = (1 :: xs) reduceLeft {(x, y) => x * y}
ScalaライブラリのreduceLeftの実装はこのようになっていて、foldLeftを使っている。
def reduceLeft(op: (A, A) => A): A = this match {
  case Nil => error("Nil.reduceLeft")
  case x :: xs => (xs foldLeft x)(op)
}
def foldLeft[B](z: B)(op: (B, A) => B): B = this match {
  case Nil => z
  case x :: xs => (xs foldLeft op(z, x))(op)
}
Foldは少し分かりづらいけど、初期値を渡せるreduceのような感じ。同じようにreduceRightやfoldRightがあるけど、適用順序が右からになるだけなので省略。foldLeft・foldRightは別名があり、それぞれ「/:」「:\」とも書ける。

Nested Mapping
例えば、与えられたnに対して 1 ≦ j < i < n かつ i + jが素数になるようなすべてのiとjの組み合わせを探したいとする。具体的に言うと、n=7の場合には、(2, 1), (3, 2), (4, 1), (4, 3), (5, 2), (6, 1), (6, 5)となる。 
これを実現するためには、まずはnより小さなすべての(i, j)の組み合わせを作り出し、それに対してFilterをかければよい。
ここでは、どうやってn以下のすべての(i, j)の組み合わせを作るかということにフォーカスするが、これは以下のようにすれば作ることができる。
List.range(1, n)
.map(i => List.range(1, i).map(x => (i, x)))
.foldRight(List[(Int, Int)]()) {(xs, ys) => xs ::: ys}
.filter(pair => isPrime(pair._1 + pair._2))
何をやっているかというと…
  1. まず最初のList.range(1, n)で1からnまでのListを作る。
  2. 次の.map(i => List.range(1, i)でList(1..n)の要素をList(1), List(1, 2), List(1, 2, 3)...というように置き換えたListを作る。この時点で、Listの中にListが入ったネスト構造になっている。
  3. 次の.map(x => (i, x))でList内のListに対してList(1) -> List((1, 1)) List(1, 2) -> List((2, 1), (2, 2))...というように、タプルに置き換えていく。この段階で、List(List((1,1)), List((2, 1), (2, 2)), List((3, 1), (3, 2), (3, 3)), ....)のような状態になっている。
  4. 最後に、foldRightを使って内部のListを連結してやる。
  5. めでたくList((1, 1), (2, 1), (2, 2), (3, 1), (3, 2), (3, 3), ....)という構造物が出来る。
という感じなのだが、こんなの自分じゃ思いつかない…やっぱこういうのは数学…というより数字に強い人が得意なんだろうか。まぁ、こんなこともできるよ、ということで。

Flattering Maps
Map+内部Listの連結というのはよく使う組み合わせらしく、既にメソッドが用意されている。それがflatMap。flatMapを使えば、先ほどのメソッドは
List.range(1, n)
.flatMap(i => List.range(1, i).map(x => (i, x)))
.filter(pair => isPrime(pair._1 + pair._2))
こうなる。map操作が、List内の各要素に対してListを返す場合に有効ということらしい。それが"よくある"と言われてもよくわからないのは、自分がまだ関数型言語に慣れていないせいなんだろうな。

配列との比較、再び
手続き型で一般的に使われる配列とは違って、Listのインデックスアクセスは(applyで可能であるにも関わらず)滅多に使わない。これは、Listのインデックスアクセスが非効率であるということに加えて、用意されているメソッド群を組み合わせて操作した方が便利だから。

確かに、ScalaのListに幾分慣れた後でJavaの(Array)ListをIteratorやindex使ってアクセスしたりFilterしたりしているのを見ると、イライラしてくる。Commons CollectionsのCollectionUtils.filterやCollectionUtils.forAllDoを使えば似たようなことはできるんだけど、どちらもList自体に変更を加えちゃうし、戻り値はvoidだしでいまいち使いづらい。

2008年10月16日木曜日

Scalaのリスト-1階メソッド-

続いては、Listの1階操作。

要素の取得
まずは、List要素を取得するようなやつらから。
  • isEmpty
  • head
  • tail
  • length
  • last
  • take
  • drop
  • split
  • apply
  • init
おなじみですね。じゃ、次。というのもアレなので、それぞれのメソッドの実装を見てみる。
def isEmpty: Boolean = this match {
  case Nil => true
  case x :: xs => false
}

def head: A = this match {
  case Nil => error("Nil.head")
  case x :: xs => x
}

def tail: List[A] = this match {
  case Nil => error("Nil.tail")
  case x :: xs => xs
}
この辺はそのまんまという感じ。
def length: Int = this match {
  case Nil => 0
  case x :: xs => 1 + xs.length
}

def last: A = this match {
  case Nil => error("Nil.last")
  case x :: Nil => x
  case x :: xs => xs.last
}

def take(n: Int): List[A] = if (n == 0 || isEmpty) Nil else head :: tail.take(n1)

def drop(n: Int): List[A] = if (n == 0 || isEmpty) this else tail.drop(n1)
このあたりから再帰が入ってくる。正直自分も関数型にはあまり慣れてないんだけど、ちょっとずつわかってきた気がする。手続き型のように問題をトップダウンで分解するというよりは、解が明らかな状況から積み上げるように考える…ような。
def split(n: Int): (List[A], List[A]) = (take(n), drop(n))

def apply(n: Int): A = drop(n).head
定義済みの関数を使えば簡単に書ける。

挿入ソート
ScalaByExampleから挿入ソートのサンプルを転載。
def isort(xs: List[Int]): List[Int] = xs match {
  case List() => List()
  case x :: xs1 => insert(x, isort(xs1))
}

def insert(x: Int, xs: List[Int]): List[Int] = xs match {
  case List() => List(x)
  case y :: ys => if (x <= y) x :: xs else y :: insert(x, ys) }
うーん、いいね。やっぱこうでないと。Listのインスタンスはすべてcaseクラスだから、こゆ風になるのね。caseパターンのバリエーションについてはもう少し知りたいところだけど、それはまた別の機会にちゃんと調べることにする。

ZIP
ホグワーツ違法学校が浮かんでくるが、それはまぁ良いとして…ZIPです。同じ長さの複数のListを縦(?)に結合して1つのListとして返すもの。
def zip[B](that: List[B]): List[(a,b)] = if (this.isEmpty || that.isEmpty) Nil else (this.head, that.head) :: (this.tail zip that.tail)
こんな感じ。

Listの結合
演算子「:::」を使う。これまた、Listのメソッド。正直、「::」とどう違うのかよくわかってません ^^;
def :::[B >: A](prefix: List[B]): List[B] = prefix match {
  case Nil => this
  case p :: ps => this.:::(ps).::(p)
}
リバース
reverseメソッドを使う。
def reverse[A](xs: List[A]): List[A] = xs match {
  case Nil => Nil
  case x :: xs => reverse(xs) ::: List(x)
}
:::を使ってうまいこと書いてある。

マージソート
例として、マージソートが書いてある。わからないところはないんだけど、1から自分で書けるだろうか。
def msort[A](less: (A, A) => Boolean)(xs: List[A]): List[A] = {
  def merge(xs1: List[A], xs2: List[A]): List[A] =
    if (xs1.isEmpty) xs2
    else if (xs2.isEmpty) xs1
    else if (less(xs1.head, xs2.head)) xs1.head :: merge(xs1.tail, xs2)
    else xs2.head :: merge(xs1, xs2.tail)
  val n = xs.length/2
  if (n == 0) xs
  else merge(msort(less)(xs take n), msort(less)(xs drop n))
}

2008年10月15日水曜日

Scalaのリスト-概要と構築方法-

概要
配列のような構造物。List(x1, x2,...xn)というように使うことができる。…良く見てみると、new List() じゃなく、List() なのね。applyを使ってるのだろうか?ソース見てみると…
object List {

def apply[A](xs: A*): List[A] = xs.toList
ビンゴっぽいけど、よくわからない点がいくつか。まず、Listはclassじゃなくobject?まじか?と思ったら、下の方には…
sealed abstract class List[+A] extends Seq[A] with Product
とclass Listの定義があり、ここにListのメソッド群が定義されていた。object Listのクラスコメントを見るとわかるのだが、object Listはclass Listのインスタンスを生成するためのFactoryのようなもので、記法上のわかりやすさからclass Listと同じ名前になっている(が、継承関係はまったくない)ようだ。なんかすごいな。こんなのJavaではお目にかかったことがないぞ。
もう1つのわからない点は、xs: A*の「*」。これは後で説明する。

Listクラス階層
Listクラスには、「Nil」と「::」の2つのサブクラスしかなく、このいずれもがcaseクラスとなっている。さらに、Nilはシングルトンで良いため、実際にはobjectとして定義されている。

Listの特徴
Listのクラス定義でわかるとおり、ListはHomogenious。つまり、List内の要素は同じ型でないといけない。さらに、Listは共変。Listの配列との大きな違いは以下の3つらしい。
  • List内の要素は、割り当てによって変更できない
  • 再帰的な構造をとることができる
  • Listを操作する機能が豊富
Listの構築
さて、あらためてListの構築。以下の方法が主なものらしい。
  • List()を使う
  • :: Nilを使う
1番目の方法は、Listオブジェクトのapply()を利用するもので、先ほども少し触れたのだが、
  def apply[A](xs: A*): List[A] = xs.toList
この部分で実現されている。なぜxsに対してtoListが呼び出せるのかはよくわからない。A*が何か関係してるのだろうか。わからんわからん言っててもしょうがないので、Language Specificationで調べてみたところ、これはRepeated Parameterといって、繰り返しを表すものらしい。で、この型はscala.Seq[+A]になるようだ。でも、SeqにはtoListなんてないんだけどどうなってんだ?検索してみると、ListBufferがtoListを持っているようだ。xsは結局ListBufferになっているんだろうか?謎は深まるばかりなり。

2番目の方法は、:: 演算子を使う方法。といってもこれはListクラスのメソッドで、「:: Nil」はNilオブジェクトに対して::メソッドを呼び出す形になる。::メソッドはというと…
  def ::[B >: A] (x: B): List[B] =
new scala.::(x, this)
こうなっている。一見するとわけがわからないが、ここでの「::」はメソッドではなくListのサブクラスの方。で、コンストラクタパラメータとしてxとthisを渡して::インスタンスを作っているわけだ。よくこんなことするよ、ほんと。

Scalaでは、演算子はすべてメソッドとして実装されており、実際はメソッド呼び出しに展開される。この中でも「:」で終了する演算子(メソッド)は特別な扱いとなり、これは右結合の演算子として扱われる。
にもかかわらず、評価は左から順に行われることになっているため、D :: Eは{val x = D; E.::(x)}に変換されるらしい。単純にE.::(D)に変換しちゃうと、Eから先に評価されちゃうからねー。

ここまでわかれば、Listの構築に関しては完璧だろう!たぶん!Listは長いので、いったんここで終了。各種操作は次回エントリにまわします。なんかHaskellの復習してるみたいでいまいちつまんないんだけどね…。

2008年10月14日火曜日

Scalaの総称クラスと総称メソッド-残りまとめて-

Upper Bounds/Lower Bounds
型パラメータ制限について。二つあわせて軽く説明しておく。
  • Upper Boundsは「T <: U」のように使う。TはUのサブタイプでないといけないという制限。
  • Lower Boundsは「T >: S」のように使う。TはSのスーパータイプでないといけないという制限。
Javaにもほとんど同じものがある(? extends Tとか ? super Tとか)ので、あまり説明はいらない(Javaの文献読んだ方が早い)と思う。さっさといきます。
共変について扱ったエントリで、Co-Variantの制限が強すぎる場合の対処はLower Boundsで可能という話をしたけども、それがこれ。
class Stack[+A] {
  def push[B >: A](x: B): Stack[B] = new NonEmptyStack(x, this)
このようにすることで、Contra-Variantなポジションに(間接的に)Co-Variantな型を登場させることができる。ちなみに、総称メソッドの型パラメータ制限はCo-Variantポジションらしい。

しかし、Stack[String]にAnyRefをpushできるというのはなんだか妙な感じだ。AnyRefをpushすると、Stack[AnyRef]が返される。確かに返ってきたStackは型的にはあっているんだけど、慣れてないせいかなんだか奇妙に感じてしまう。
それにしてもJavaにそっくり…って、Scala作ってる人ってJavaのGenericsの仕様策定にも深くかかわってるんだっけ。Javaの上で動いてるし、似てるのも当たり前か。

Least Types
Scalaでは、objectは総称性を持つことができない。EmptyStackはSingletonでよいのでobjectとして定義したいし、EmptyStackは何を入れてもいいので総称性を持たせたい。が、Scalaではこれはできないということになる。こういうときには、Nothingを使う。Nothingはすべてのクラスのサブクラス(!)なので、Stack[+A]のようなCo-Variantなクラスにおいて、Stack[Nothing]は(あらゆる)Stack[T]のサブクラスになる。便利。
これを使えば、EmptyStackには何でもつめられるということになる。
val s = EmptyStack.push("abc").push(new AnyRef())
Tuples
タプル。組ですね。順序を保持した値の集まりで、実体はTuple1~22クラス。Scalaでは、組の要素は22個までということだろうか。タプル内要素の型は同じでなくてもかまわない。ちなみに、共変。
Scalaではシンタックスシュガーが設けられており、TupleX(x1, x2..., xn)のかわりに(x1, x2...., xn)というように書ける。
値を取り出す場合は、コンストラクタパラメータの取り出し方法(tuple._1, tuple._2)か、パターンマッチを使う。

Function
Scalaは関数をFirst-class Valueとして扱う。で、ScalaではすべてがObject。よって、FunctionもObject。タプルと同じように、実体はFunction0~22クラス。引数はCo-Variant、戻り値はContra-Variant。
f(x)は、f.apply(x)呼び出しに変換される。

最後に、ちょっと頭がこんがらがることを…まずはこの例。
val f: (AnyRef => Int) = x => x.hashCode()
val g: (String => Int) = f
g("abc")
この例では、gにfを代入して使うことができている。よって、gはfのスーパータイプ(fはgのサブタイプ)だと言える。ここでg/fを構成する型を見てみると、fの引数がgの引数のスーパータイプになっている。つまり、継承関係が逆転している。これも、Contra-Variantと言うらしい。一般に、関数に関しては以下が成立する。
S => T is a subtype of S' => T', provided S' is a subtype of S and T is a subtype of T'
S'がSのサブタイプであり、TがT'のサブタイプであるとき、S => T は、S' => T'のサブタイプである
引数と戻り値のサブタイプ関係がねじれており、Contra-Variantなのは引数だけになっていることに注意。戻り値はCo-Variant。頭こんがらがる。

Scalaの高階関数-カリー化-

Scalaのカリー化に納得がいかなかった(しちゃんと試してなかった)ので、サンプルを作りながら試していく。基本的にはScalaByExampleをパクりながら ^^;

実験素材
さて、まずはsum。面倒だったのでtraitとして実装。一番基本的な形での定義。これはScalaByExampleにも書かれてている通り、1引数の関数fを渡すと、2引数の関数を生成して返す関数。返された関数の引数として「開始」と「終了」の数を指定して呼び出すと、その間の整数1つ1つに対して関数fを適用し、すべて足し合わせて返してくれるというもの。説明めんどくさい、サンプル間違えたかな…まぁいいや。
trait CurryingSample {
  def sum(f: Int => Int): (Int, Int) => Int = {
    def sumF(a: Int, b: Int): Int =
      if (a > b) 0 else f(a) + sumF(a + 1, b)
    sumF
  }
}
お次は呼び出しコード。
object CurryingMain extends CurryingSample {
  def main(args : Array[String]) : Unit = {
    val simpleSum = sum(x => x)
    println(simpleSum(0, 5))
  }
}
このサンプルでは、各数に適用する関数は匿名関数 x => x。つまり、引数をそのまま返す関数。というわけで、ここで生成されている関数「simpleSum」は、指定された範囲の整数を単純に足し合わせて返す。実行してみると…
15
となる。オッケー。ここまでは、ScalaByExampleで書いてあることだし、わかっていた。問題は、カリー化の対象になる関数をどのくらい柔軟に記述できるか。じゃ、試していく。

短縮記法
まずは、ScalaByExampleに書いてあった短縮記法。traitに以下を追加。
def shortSum(f: Int => Int)(a: Int, b:Int) : Int = {
  if (a > b) 0 else f(a) + shortSum(f)(a + 1, b)
}
呼び出し側を以下のように変更。
object CurryingMain extends CurryingSample {
  def main(args : Array[String]) : Unit = {
    val simpleSum = shortSum(x => x)
    println(simpleSum(0, 5))
  }
}
これは、コンパイルエラーになる。メッセージはこんな感じのもの。
missing arguments for method shortSum in trait CurryingSample;
follow this method with `_' if you want to treat it as a partially applied function
どうやら、この記法の場合は末尾に「_」をつけなくてはCurry化かどうか推測できないみたいだ。まぁ、もともとカリー化する場合には「_」をつけなければならず、場合によって省略できるということになっているのでいいのか?
気を取り直して、コンパイルがとおるようになおす。
  val simpleSum = shortSum(x => x)_
コンパイルが通るようになり、実行すると以下のように出力される。
15
オーケー。

高階関数を普通の関数として呼び出し
次は、shortSumを高階関数じゃなく普通の関数として呼び出せるかどうか。呼び出し側を以下のように変える。カリー化じゃなくて、値を全部渡す。
def main(args : Array[String]) : Unit = {
  val simpleSum = shortSum(x => x)(1, 2)
  println(simpleSum)
}
コンパイルは通る。実行すると…
3
正常に動いている。高階関数だからといって、高階関数専用になってしまうわけではないらしい。これはちょっと嬉しい。sumでも同様に問題なく動作する。

部分適用範囲の変更
次は、適用する引数を増やそうとしてみる。まずは今のままで、呼び出し側を変更する。
  val simpleSum = shortSum(x => x)(1)_
1引数目を1で固定した関数を作ろうとした…が、当然のごとくコンパイルエラー。そりゃそうか。
wrong number of arguments for method shortSum: (Int,Int)Int
うーん、エラー表記の意味がわからん…shortSumの型が(Int,Int)Intってどういうことだ?shortSumの型は(Int => Int) => Intみたいな型のはずだけど…それをこう書くの?よくわからん。とりあえず引数の数が違うらしい。範囲を部分適用できるように、shortSumを修正してみる。
def shortSum(f: Int => Int)(a: Int)(b:Int) : Int = {
  if (a > b) 0 else f(a) + shortSum(f)(a + 1)(b)
}
範囲を表すaとbのそれぞれを()で囲み、別々の引数となるようにしてみた。さて、これで部分適用は可能か。呼び出し側。
def main(args : Array[String]) : Unit = {
  val simpleSum = shortSum(x => x)(1)_
  println(simpleSum(5))
}
おぉ、コンパイル通る。ちなみにこの場合も、「_」をつけないとコンパイルエラーになる。実行してみると…
15
きた!

わかったこと
結局は、部分適用したい単位ごとに()で囲わないといけないということか。やっぱりHaskellみたいにフリーダムじゃないのね。とはいえ、思っていたよりは使えそうかも。安心した。部分適用するかもしれない関数の引数は、なんでもかんでもバラバラに()で切り刻んでおけばOK?本当か?

Scalaの総称クラスと総称メソッド-共変-

いわゆるGenericsのメカニズムについては、ごく基本的な部分についてはJavaと同じように見える。型パラメータの境界(親の型としてXXXを持ってないといけないとか、子の型としてXXXを持ってないといけないとか)についても同じ。大きく違うのは、型パラメータの継承関係と、パラメータバウンドされた総称クラス(以下、面倒なので"総称クラス"と書く)自体の継承関係との関係、すなわち共変(Co-Variant)に関する部分。

といっても、共変自体にあまり詳しくないので、しばらく共変について勉強してみる。

共変
調べても定義らしい定義が出てこなくて困った。とりあえずは関連ありそうな記事として以下のようなものがあった。
このあたりの記述を参考にすると、共変というのは「型パラメータの継承関係(代替性)が、総称クラスにまで伝搬するかどうかを表す性質」と考えてよさそうだ。関数型言語のページとか見てるともう少し広い考えのような気もするけど、とりあえず今のところはこういう理解で進んでみる。
それにしても、JavaのGenricsは既にものすごくややこしい…ワイルドカード、キャプチャ、境界、このあたりを正確に理解している人間はいったいどれくらいいるのだろうか。

Variance-Annotation
訳し方がわからなかったのでそのままのタイトルにしてしまった。これは要するに、共変に関する性質を変更できる注釈(Annotation)のこと。どのように変更できるかというと…
  • Non-Variant(共変しない(Javaと同じ))
  • Co-Variant(共変する)
  • Contra-Variant(マイナス方向に共変する)
では、順番に。

Non-Variant
これは、Stack[AnyRef]型の変数にStack[String]型のオブジェクトを代入しようとするとエラーになるということ。Scalaの総称クラスはデフォルトでは共変しない。JavaのGenericsも同様。実際にこんな風に書いてみると…
package generics

abstract class Stack[A] {
  def push(x: A): Stack[A] = new NonEmptyStack[A](x, this)
  def isEmpty: Boolean
  def top: A
  def pop: Stack[A]
}

class EmptyStack[A] extends Stack[A] {
  def isEmpty = true
  def top = error("EmptyStack.top")
  def pop = error("EmptyStack.pop")
}

class NonEmptyStack[A](elem: A, rest: Stack[A]) extends Stack[A] {
  def isEmpty = false
  def top = elem
  def pop = rest
}
で、まずは以下のように書いてみる。
  val stack : Stack[String] = new EmptyStack[String]
  println(stack.push("aiueo").top)
当然コンパイルは通る。型パラメータは同じなんだから当然。で、実行すると…
  aiueo
こうなる。今度はこんな風に書いてみると…
  val stack : Stack[AnyRef] = new EmptyStack[String]
今度はコンパイルエラーになり、以下のようなエラーメッセージが出る。
  type mismatch;
  found : generics.EmptyStack[String]
  required: generics.Stack[AnyRef]
型パラメータどうしは継承関係にあるけど、総称クラスどうしは継承関係にはならないことがわかる。ちなみに、このように書くと…。
  val stack = new EmptyStack[AnyRef]
  println(stack.push("aiueo").top)
当たり前だがコンパイルは通り、以下のように結果が出力される。
  aiueo
この場合は型推論が働いて、stackの型はEmptyStack[AnyRef]になるので型の不一致は起きない。"aiueo"はAnyRef型のサブタイプなので、メソッド呼び出しに関しても問題なし。これが、共変なしの状態。次。

Co-Variant
繰り返しになるがこれは、「型パラメータが継承関係にあれば、総称クラスも継承関係を持つ」というもの。具体的には、Stack[AnyRef]がStack[String]のスーパータイプになるということ。型パラメータにAnnotation「+」を付加することで、共変になる。Javaで言うと、配列が共変といえる。Object[] array = new String[5]はOKでしょ?じゃ、Scalaのサンプルいきます。

まずは、Stackの定義をこんな風に書き換えてみる。といっても、クラス定義の型パラメータに「+」をつけただけ。
abstract class Stack[+A] {
  def push(x: A): Stack[A] = new NonEmptyStack[A](x, this)
  def isEmpty: Boolean
  def top: A
  def pop: Stack[A]
}
すると、
  covariant type A occurs in contravariant position in type A of value x
こんなコンパイルエラーが出る。なんのこっちゃ?という感じだが、少し説明を。Javaの配列が共変だということは既に触れたわけだけど、Javaの配列でこーんなことをすると、コンパイルエラーは起きず、実行時例外が発生する。
  Object[] array = new String[5];
  array[0] = 0;

  Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
at generics.ArraySample.main(ArraySample.java:6)
仮にこのコードが実行可能だとすると、arrayの実体であるString配列にintを格納することになり、型システムが崩れてしまう。Javaではこれを、コンパイル時のチェックではなく実行時チェックによって防いでいるためRuntimeExceptionが発生するわけだが、Scalaはコンパイル時にチェックするようになっている。言語仕様的には、共変の型を配置できる場所が決まっており、これはCo-Variantポジションと言うらしい。Co-Variantポジションは…
  • 変数の型
  • メソッドの戻り値の型
  • 他のcovariant型への引数
のいずれかであると決められており、これ以外の場所にCo-Variantな変数を書くとコンパイルエラーになる。さきほどの例では、def push(x: A): Stack[A] で引数の型としてCo-Variantの変数が書かれていたためにコンパイルエラーになったというわけ。引数でわたってくるデータは内部状態を変更する可能性があるので、禁止しているわけだ。うーん、保守的。でもこれが、Co-Variantのデフォルトの動作。
実際にはpushは内部を変更するわけではなく、新しいStackを作って返すだけなので、エラーになるのは納得いかない。これは、Lower Boundsで解決方法が書かれるらしいので、とりあえずは先まわし。

さて、気をとりなおして…Stackからpushメソッドを削除し、以下のようにする。もうStackでもなんでもないが、型システムを理解するのが目的なのでこれはこれでよしとする。
abstract class Stack[+A] {
  def isEmpty: Boolean
  def top: A
  def pop: Stack[A]
}

class EmptyStack[A] extends Stack[A] {
  def isEmpty = true
  def top = error("EmptyStack.top")
  def pop = error("EmptyStack.pop")
}

class NonEmptyStack[A](elem: A, rest: Stack[A]) extends Stack[A] {
  def isEmpty = false
  def top = elem
  def pop = rest
}
使う側のコード。
  val stack : Stack[AnyRef] = new NonEmptyStack[String]("aiueo", new EmptyStack[String])
  println(stack.top)
これは、コンパイルを通る。Stack[AnyRef]がNotEmptyStack[String]のスーパータイプになっていることがわかる。実行すると…
  aiueo
正常に出力される。最後、Contra-Variant。

Contra-Variant
これは今までの常識を超えた感じのもので、共変の反対になる。何が反対かというと継承階層の上下で、型パラメータがスーパータイプのものは総称クラスでサブタイプになり、型パラメータがサブタイプのものは総称クラスでスーパータイプになる。思わずポルナレフのAAが出てきそうになるが、例を出すとわかりやすい。Contra-Variantとは…
  val stack : Stack[String] = new Stack[AnyRef]
このコンパイルが通るようなもの。逆に、
  val stack : Stack[AnyRef] = new Stack[String]
これはコンパイルエラーになる。
  type mismatch;
  found : generics.contra_variant.EmptyStack[String]
  required: generics.contra_variant.Stack[AnyRef] StackMain.scala
どこで使うんでしょ、これw Contra-Variantな型にするためには、型パラメータに「-」をつける。Co-Variantと同様、Non-VariantなStackを改良してみる。同じく、クラス定義の型パラメータに「-」をつけただけ。
abstract class Stack[-A] {
  def push(x: A): Stack[A] = new NonEmptyStack[A](x, this)
  def isEmpty: Boolean
  def top: A
  def pop: Stack[A]
}

class EmptyStack[A] extends Stack[A] {
  def isEmpty = true
  def top = error("EmptyStack.top")
  def pop = error("EmptyStack.pop")
}

class NonEmptyStack[A](elem: A, rest: Stack[A]) extends Stack[A] {
  def isEmpty = false
  def top = elem
  def pop = rest
}
すると、予想通り(?)コンパイルエラーになる。
  contravariant type A occurs in covariant position in type => A of method top
もうわかったと思うが、Contra-Variantな型もCo-Variantな型と同様に記述できる場所が決まっており、これはContra-Variantポジションと言うようだ。サンプルのメッセージとこれまでの説明からすると、恐らくこれはCo-Variantと反対で、引数パラメータの中にしか許されない…のかな。ScalaByExampleでは深く触れられていないのでここでは深入りしないことにする。topを削除して…
abstract class Stack[-A] {
  def push(x: A): Stack[A] = new NonEmptyStack[A](x, this)
  def isEmpty: Boolean
  def pop: Stack[A]
}

class EmptyStack[A] extends Stack[A] {
  def isEmpty = true
  def pop = error("EmptyStack.pop")
}

class NonEmptyStack[A](elem: A, rest: Stack[A]) extends Stack[A] {
  def isEmpty = false
  def pop = rest
}
これでコンパイルは通った。使う側のコードは…
  val stack : Stack[AnyRef] = new EmptyStack[String]
まずはこう書くと、コンパイルエラーになる。まぁ予想どおり。で、こうすると…
  val stack : Stack[String] = new EmptyStack[AnyRef]
コンパイルを通る。やっぱり妙な感じ。