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。頭こんがらがる。

コメント