scalaで型パラメータや変位指定について色々調べていくうちに学んだことや面白いと思ったことをつらつら書き残しておこうと思います。
(*) 私は言語に特別詳しいわけでもないので「こうじゃないかな?」といった憶測を多分に含んでいます。
ちょっとその前に
継承と派生型
ぶっちゃけ継承や派生型を語るには知識が足りなさすぎるのですが、それは置いておいて今回大事になるのは以下のような関係です。
scala> val i: Int = 123
i: Int = 123
scala> val a: Any = i
a: Any = 123
Int
型をAny
型に代入しています。Int
はAny
(正確にはAny
を継承しているAnyVal
)を継承しているので、基本型であるAny
型の変数に代入が可能です。これは基底クラスであるAny
の機能を派生クラスであるInt
が全て引き継いでいる(というか強制している)ため可能なのだと思われます。つまり、Int
をAny
として扱っても問題ないということでしょう。またInt
がAny
の派生であるということは以下のような包含関係をイメージできるかと思います。

余談 その1
これを機会に継承や派生型をもう一度ちゃんと調べて理解しておこう!と思ったは良いのですが、これらの話の歴史・闇・沼が深すぎてびっくりです。いやー、プログラミング言語って凄いですね。
型パラメータ
では早速型パラメータについてです。githubでライブラリを調べるとしょっちゅう出てきますね。Javaやswiftではジェネリクス(総称型)と呼ばれてますね。ここでは仮にMyContainer
を定義していきたいと思います。
class MyContainer[A](val content: A)
val intContainer = new MyContainer(1)
val stringContainer = new MyContainer("string")
どんな型でも入れられるガバガバ状態ですね。ではここでMyContainer
で扱うためのBaseContent
というクラスと、その派生クラスであるDerivedContent
を用意したいと思います。
class BaseContent
class DerivedContent extends BaseContent
何ともまぁ手抜きなクラスですが、こんな手抜きも許してくれるscalaコンパイラに感謝ですね。このBaseContent
とDerivedContent
には継承関係があるので以下のようなイメージになるかと思います。

ではこのDerivedContent
を型パラメータとしてとるMyContainer[DerivedContent]
はMyContainer[BaseContent]
の派生型として扱えるでしょうか。答えは「扱えない」です。
val content1 = new DerivedContent()
val container1 = new MyContainer(content1)
val container2: MyContainer[BaseContent] = container1 // compile error
ですが、scalaのコンパイラはとても賢いことに「君のやりたいことはもしかして〜」と提案してくれます。そのエラー内容が以下です。
error: type mismatch;
found : this.MyContainer[this.DerivedContent]
required: this.MyContainer[this.BaseContent]
Note: this.DerivedContent <: this.BaseContent, but class MyContainer is invariant in type A.
You may wish to define A as +A instead. (SLS 4.5)
val container2: MyContainer[BaseContent] = container1
^
one error found
「MyContainer
は型Aにおいてinvariant
だよ」と教えてくれてます。このinvariant
が日本語で言う「不変(または非変)」らしいですね。
変位指定
ではコンパイラに怒られた不変とは何か?ですが、型パラメータの性質の1つみたいですね。型パラメータの性質として以下のようなものがあるそうです。
- 共変(covariant):型パラメータの派生型(または同一型)を許容。
+A
のように定義する。 - 反変(contravariant):型パラメータの基本型(または同一型)を許容。
-A
のように定義する。 - 不変(invariant):型パラメータの同一型のみ許容。
A
のように何も指定せずに定義する。
これまでのMyContainer[A]
は不変だったことが分かりますね。ゆえにMyContainer[BaseContent]
型へのMyContainer[DerivedContent]
型の代入はエラーだったんでしょう。また、上記よりMyContainer
の型パラメータA
は共変性を持つように+A
という指定をすべきことが分かります。(というかコンパイラもそう言ってますね、本当に賢いやつです。)
余談 その2
「共変 反変」でググると最初にベクトルのお話が出てきます。ゆえに一時期まで私は勝手に「共変と反変はベクトル由来の概念なのかな?」と思っていました(内緒です)。ですがwikipediaによると、これまた圏論由来とありびっくり。さすが圏論、本当どこにでも出てきますね〜。(そもそも圏論の共変と反変自体がベクトルの話由来なのかもしれませんけどね)
共変
では先程のコンパイラ先生の言う通りに変更して共変性なるものを試してみたいと思います。
class MyContainer[+A](val content: A)
class BaseContent
class DerivedContent extends BaseContent
val content1 = new DerivedContent()
val container1 = new MyContainer(content1)
val container2: MyContainer[BaseContent] = container1
コンパイル通りました!これでOreOreList
が作れるぜ!
ですがなぜわざわざ+A
などという変位指定を加えなければならないのでしょうか。「全部共変でいいじゃん」と思う方もいらっしゃるかと思います。私も最初は謎だったのですが、調べていくとJavaの配列が分かりやすい例として挙げられていました。
public class Test {
public static void main(String args[]){
String[] strList = {"string"};
Object[] objList = strList;
objList[0] = 1;
}
}
こちらのJavaの簡単な例ですが、コンパイルは通りますが実行時エラーになります。それもそのはず、String[]
型の要素にInteger
型の値を入れているのですから。これはそもそもJavaの配列がミュータブルにも関わらず共変であるためだそうです。このような事態を防ぐためにも「ミュータブルな型は非変でなければならい」という設計にしなければならないそうです。逆に、イミュータブルであれば(参照のみなので)共変でも大丈夫だそうです。ゆえに、scalaではイミュータブルなList
は共変、ミュータブルなArray
は非変となっているそうですね。
scala> val list1 = List(1, 2, 3)
list1: List[Int] = List(1, 2, 3)
scala> val list2: List[Any] = list1
list2: List[Any] = List(1, 2, 3)
scala> val array1 = Array(1, 2, 3)
array1: Array[Int] = Array(1, 2, 3)
scala> val array2: Array[Any] = array1
<console>:12: error: type mismatch;
found : Array[Int]
required: Array[Any]
Note: Int <: Any, but class Array is invariant in type T.
You may wish to investigate a wildcard type such as `_ <: Any`. (SLS 3.2.10)
val array2: Array[Any] = array1
^
余談 その3
「ミュータブルな型は不変でなければ〜」みたいに書くととてもややこしいですねw 「それはミュータブルなの!?イミュータブルなの!?」とつっこまれそうです。immutableもinvariantも日本語だと「不変」と訳すそうですが、invariantが「非変」とも呼ばれているのはこういった曖昧さを回避するためなんですかね?
あ、あとJavaを悪い例みたいに挙げてしまいましたが、決してJavaをディスりたいわけではないです(念の為)。viva らぶあんどぴーす。
反変
初めて変位指定を知ったとき、正直「共変はまだしも反変は何の役に立つんだ?」と思ってました。そういう時期が、私にもありました。ですが有名な某コップ本にもあるように、関数の定義において知らず知らず使われていたんですね。では実際にscalaの関数の定義を見てみたいと思います。
trait Function1[-T1, +R] extends AnyRef {
def apply(v1: T1): R
}
実際の定義から少々、いやかなーり勝手に省略した形ですが、引数を1つとる関数の定義です。大事なのはFunction1[-T1, +R]
の-T1
および+R
です。引数の型(-T1
)が反変、返り値の型(+R
)が共変となっています。これは関数の派生型の一般的なルールとして以下が成り立つためだそうです。以下の x, x', y, y' は型を、x' ⊆ x は x' が x の派生型であることを、x → y は引数の型が x 返り値の型が y の関数を意味しています。(たしか何かの論文からの引用なのですがオレオレメモに式しか残ってない・・・)
x' \subseteq x, y \subseteq y' \Rightarrow x \rightarrow y \subseteq x' \rightarrow y'
まんまFunction1
の定義と同じではないでしょうか。以下のように実例を手元で試すのが分かりやすいと思います。
scala> val func1: Any => Int = x => 1
func1: Any => Int = $$Lambda$1108/1312250810@ab2e6d2
scala> val func2: Int => Int = func1
func2: Int => Int = $$Lambda$1108/1312250810@ab2e6d2
scala> val func3: Int => Any = func2
func3: Int => Any = $$Lambda$1108/1312250810@ab2e6d2
(引数をガン無視するので)引数を取る意味が全くないというセンスの欠片もないfunc1
ですが、func2
およびfunc3
に代入できてしまいます。これはfunc1
がfunc2
(およびfunc3
)の派生型であるからです。つまり、func1 ⊆ func2 ⊆ func3
であり、以下のようにイメージして良いと思います。

また基本型(func3
)を派生型(func1
)で置換しても問題ないということから、scalaの関数(の型定義)はリスコフの置換原則を満たしている、ということになるのではないでしょうか(より正確にはscalaを作っている方々がリスコフの置換原則を満たすように関数の型を定義している、とかでしょうか?)。「反変とか何の役に立つんだ?」とか思ってたくせにガッツリお世話になってましたね。(むしろ反変ってこんなにデキるやつだったんですね)
補足
もしかしたら(いやもしかしなくとも)関数の置換部分が分かりにくいかもしれないので、念の為補足しておきます。以降ではfunc1
とfunc3
のみ考えます。まず引数について見たいと思います。
scala> func3(1)
res2: Any = 1
scala> func3("a")
<console>:13: error: type mismatch;
found : String("a")
required: Int
func3("a")
^
scala> func1(1)
res4: Int = 1
scala> func1("a")
res5: Int = 1
func3
の引数の型はInt
、func1
の引数の型はAny
です。func1
をfunc3
として扱う場合、func1
にはInt
が入ると思われますが、func1
の引数の型はAny
であるため問題ありません。次に、返り値も見てみたいと思います。
scala> val a1: Any = func1(2)
a1: Any = 1
scala> val a2: Any = func3(2)
a2: Any = 1
func3
の返り値はAny
、func1
の返り値の型はInt
です。func1
をfunc3
として扱っても、Any
を返す想定なのでInt
を返しても問題ありません。ここは「ちょっとその前に」の部分と同じような挙動ですね。
あとは視覚的にイメージするとしたら、ざっくりこんな感じになるんではないでしょうか?

余談 その4
なんか反変というよりは関数にフォーカスした感じになってしまった気がします。。。でも反変も関数も調べれば調べるほど「こいつ・・・デキる!」と思えてしまったんで仕方がないと思うんです。
型境界
最後は型境界についてです。型境界に関しましても共変・反変よろしく「上限境界」と「下限境界」があるようですね。
- 上限境界(upper bounds):
T <: Type
のように定義し、型パラメータ(T
)はType
の派生型(または同一型)を許容する。 - 下限境界(lower bounds):
T >: Type
のように定義し、型パラメータ(T
)はType
の基本型(または同一型)を許容する。
上限境界
上限境界の「Type
の派生型(または同一型)を許容する」ということは「少なくともType
である」と言えるのではないでしょうか。そのため、少なくともType
の持つメンバーにはアクセスできるはずです。以下はTask
という型を上限境界とした場合の例です。
trait Task {
def execute()
}
class EchoTask(val message: String) extends Task {
def execute() = println(message)
}
class Executer[T <: Task](val task: T) {
def execute() = task.execute()
}
val task = new EchoTask("message")
val executer = new Executer(task)
executer.execute()
また、上限境界では「少なくともXXXを実装している」ということを保証してくれるので、数学ちっくに「Aを満たしているのであればBが成り立つ」といった定理のような実装ができるのが超カッコいいですね。よく上限境界の例として出てくるOrdered[T]
とかは「比較可能であればソートが可能」ということを表現しているのではないでしょうか。世の中の天才は凄いですね。
余談 その5
scalaではOrdered
の他にOrdering
がありますね。こちらは型クラス(implicit parameter
)による実装みたいです。
下限境界
では最後に下限境界ですが、これまた反変と一緒で「こいつは一体なんなんだ・・・」って感じでした。しかし共変と反変を学んでいくうちに下限境界の有用性というか大事さが少し分かりました。よく書籍において「共変・反変」の次に「上限境界・下限境界」という順で記載されているのはそのためなんですかね?
話が少し逸れてしまいましたが、下限境界を学ぶためにここではOption
を模倣してMyOption
なるものを実装してみたいと思います。MyOption
の機能としてisEmpty
、get
、getOrElse
を実装したいと思います。
abstract class MyOption[+A] {
def isEmpty: Boolean
def get: A
def getOrElse(given: A): A = if (isEmpty) given else get // compile error
}
さっそく怒られました。
error: covariant type A occurs in contravariant position in type A of value given
def getOrElse(given: A): A = if (isEmpty) given else get
^
one error found
「共変な型A
が反変なポジションにあるよ」って感じでしょうか?これを何とかしてくれるのが「下限境界」みたいです。早速getOrElse
を修正します。
abstract class MyOption[+A] {
def isEmpty: Boolean
def get: A
def getOrElse[B >: A](given: B): B = if (isEmpty) given else get
}
何とかコンパイルできました。めでたしめでたし・・・ではあるのですが、このままではさっぱりです。「なぜ?」なのかが全く分かりません。ですがこのまま抽象クラスのみでは少々扱い辛いので、Some
に相当するMySome
を実装したいと思います。
class MySome[+A](val content: A) extends MyOption[A] {
def isEmpty: Boolean = false
def get: A = content
}
次にこのMySome
を使って基本型であるMyOption[Any]
の変数にMyOption[Int]
(正確にはMySome[Int]
)型の変数を代入したいと思います。
val option1: MyOption[Int] = new MySome(123)
val option2: MyOption[Any] = option1
println(option2.getOrElse("string"))
問題ありませんでした。ですが恐らくではありますが、ここでgetOrElse
の型がdef getOrElse(given: A): A
だった場合おかしなことになってるはずです。なぜならMyOption[Int]
のgetOrElse
の引数の型はInt
であるのに、MyOption[Any]
の引数の型がAny
だからです。つまり、Any => Any
であるはずなのに、実際はInt => Int
だからです。(必要ないかもですが)視覚的にイメージするとしたら以下のような感じになるかと思います。

コンパイルが通らないのであり得ないですが、仮にできたとしたらclass SomeInt extends MyOption[Int]
というクラスを作ってgetOrElse
をオーバーライドする際に引数をインクリメントした値を返すという実装をし、上記のようにMyOption[Any]
型の変数に代入してgetOrElse("String")
を呼び出せば実行時エラーになりそうな気がしますね。
というわけで、共変な型パラメータを用いて関数の引数の型を定義したい場合は、下限境界で型パラメータの基本型を許容する必要があるんですね。
余談 その6
ここではscala標準ライブラリのOptionをガッツリ参考にさせていただきました。githubにあげられてるオープンソースのプログラムはとても勉強になりますね。しかし、こういった良く言えば「参考」悪く言えば「パクり」もっと言えば「謎の改悪」のようなものを意気揚々とネットに公開するのって良いのかな・・・?出典とか引用はなるべくちゃんと書いてるつもりではあるのですが、正直どこまで許されてどこから許されないのかがよく分かっていないです・・・。
感想
調べれば調べるほど面白いですね。そろそろ年齢的にも勉強したことを片っ端から忘れていくかもしれないので備忘録として書き残しましたが、「なぜ」かを考えて書くとどうしても説明口調になってしまいましたね・・・。また、「こうじゃないかな?」という部分も書き残しましたが、実は間違っていたり、副次的なものであったり、はたまた因果が逆転していたりしないか心配です。まぁ、一番心配なのはただのこじつけではないか?ですけどね!もし野生のプロの方で「ここは違うぜ!」とか「本当はこうだ!」とかあった場合は、指摘していただけると幸いです。
参考資料
-
scala標準ライブラリ
- コミッターの方々には感謝しかありませんね。
-
TOUR OF SCALA
- 分野毎にシンプルにまとまってたり、サンプルがあったりで非常に助かりました。
-
ドワンゴさんの研修資料
- 私もこの研修受けたいです。純粋にうらやましいw
-
コップ本
- 会社で買ったりもしましたが、誰かが読んでるときに読めないのが嫌で自分用に買っちゃいました。
他にもたくさん参考にさせてもらってるんですが、さすがに書ききれないのでお許しください。