筒で理解する反変・共変
この記事では、Java、Scala、TypeScriptなど、サブタイピング(subtyping)をサポートする言語であれば間違いなくサポートしているであろう「反変(contravariant)」・「共変(covariant)」について、視覚的なアナロジーを用いつつ解説したいと思います。コード例を含め全てTypeScriptを前提とした説明ですが、同様の機能を持った言語であれば概ね同じことが言えるはずです。
そもそもサブタイピングとは
サブタイピングとは、型と型との間にサブタイプ(subtype)・スーパータイプ(supertype)という関係を定めて、スーパータイプである型の代わりとして、サブタイプである型を利用できるようにする仕組みです。
例えば、TypeScriptではstring
型はObject
型のサブタイプであるので、次のようにObject
型の変数にstring
型の値を代入することができます:
const o1: Object = "string";
一方、次のようにObject
型の値をstring
型の変数に代入することはできません。
const o2: string = "string" as Object;
// 型エラー: Type 'Object' is not assignable to type 'string'.
実際の値としては見るからに文字列型の値であったとしても、as
演算子で強引にObject
型としてマークされた値は、string
型の値として扱われないのです。
ここまでのコードをTypeScript Playgroundで試す
反変・共変とは
反変と共変は、関数やリストのような、型引数を持った複雑な型におけるサブタイプ・スーパータイプの関係を説明するために使う用語です。型引数を受け取る型が具体的な型を受け取ったとき、他の型とどのようにサブタイプ・スーパータイプの関係を成すかを表します。
共変1: 単純なプロパティーとして備えている場合
ひとまず比較的単純な、共変の例を示しましょう。
interface Covariant1<T> {
value: T;
}
上記のCovariant1
という型は、型引数T
に対して共変です。共変であるということは、Covariant1
に適当な型を渡して作った型同士 --- 例えばCovariant1<string>
やCovariant1<Object>
--- のサブタイプ・スーパータイプの関係を、渡した型同士の関係から変えないことを表します。そのため、例に挙げたCovariant1<string>
とCovariant1<Object>
では、string
がObject
のサブタイプなのでCovariant1<string>
はCovariant1<Object>
のサブタイプ、ということになります。
したがって、次のようにCovariant1<Object>
型の変数にCovariant1<string>
型の値を代入することができます:
const cov1: Covariant1<Object> = { value: "string" };
そしてその逆、Covariant1<string>
型の変数にCovariant1<Object>
型の値を代入することはできません:
const cov1Bad: Covariant1<string> = { value: "string" } as Covariant1<Object>;
// 型エラー: Type 'Covariant1<Object>' is not assignable to type 'Covariant1<string>'.
// Type 'Object' is not assignable to type 'string'.
ここまでのコードをTypeScript Playgroundで試す
このことを、サブタイプ・スーパータイプの関係より単純な、集合の大小(取り得る値の数)の関係で例えてみます。例えばObject
はstring
のスーパータイプなので、Object
はstring
より大きい、と考えます。Object
には当然string
以外の型の値が含まれますから、取り得る値の数は大きいと言えますね:
サブタイプ・スーパータイプの関係は、実際のところ全順序な関係ではない、つまりサブタイプでもスーパータイプでもどちらとも言えない場合があるので、単純な「大小」による例えは正確ではありませんが、この記事ではどちらかに該当するケースのみを取り上げるので問題ありません。
この例えにしたがってCovariant1<Object>
とCovariant1<string>
型の関係を示すと、次のようなイメージになります:
型引数として渡したObject
やstring
はCovariant1
の「付属品」として付いているイメージで、その「付属品」の大きさによってサブタイプ・スーパータイプの関係を定めています。
変数は、自身に割り当てられた型[1]の「大きさ」にフィットする、器のような役割を果たします。代入しようとしている値の型が、変数に割り当てられた型より「小さい(あるいは同じ大きさの)」場合のみ代入できるわけです。ただし、Covariant1<Object>
のような「付属品」が付いた型の場合は、「付属品」と「付属品」を付けている型両方の型が「小さく(あるいは同じ大きさで)」なければなりません。
共変2: 関数の戻り値として返す場合
Covariant1
のように、型引数の型を単純にプロパティーとして備えている場合の他、型引数の型を関数の戻り値として備えている場合も、共変となります。例えば、下記のCovariant2
は、型引数T
をプロパティーreturnValue
関数の戻り値としているので、Covariant2
は型引数T
に対して共変です:
interface Covariant2<T> {
returnValue: (value: any) => T;
}
Covariant1
と同様、Covariant2<Object>
型の変数にCovariant2<string>
型の値を代入することができますし、その逆はできません:
const cov2: Covariant2<Object> =
{ returnValue: (_value: any) => "string" };
const cov2Bad: Covariant2<string> =
{ returnValue: (_value: any) => "string" } as Covariant2<Object>;
// 型エラー: Type 'Covariant2<Object>' is not assignable to type 'Covariant2<string>'.
// Type 'Object' is not assignable to type 'string'.
ここまでのコードをTypeScript Playgroundで試す
これも大小関係で例えてみましょう。関数は値を受け取って値を返すものですから、値を入れると値が出てくる、筒のようなもので例えましょう。引数や戻り値における型の違いは、筒の入り口と出口の大きさの違いに相当するわけです。するとCovariant2<Object>
とCovariant2<string>
は次のようにイメージされます:
※ここでは引数の型は関係がないので、引数に当たる、筒の左側の部分は省略しています。
なぜこのような大小関係になるのでしょうか?実際にCovariant2<string>
のような、関数を備えた型のオブジェクトを使う側になって考えてみてください。Covariant2<string>
、つまりstring
型の値を返す関数を持ったオブジェクトのつもりで、Covariant2<Object>
、つまりstring
型よりも「大きい」値を返す関数を持ったオブジェクトを使うと、次のような問題が起こり得るからです:
図のとおり、Covariant2<string>
を使うつもりでCovariant2<Object>
に含まれる関数を呼び出すと、string
には含まれないけどObject
には含まれる型 --- 例えばnumber
--- の値が出てきてしまう恐れがあります。Object
はstring
よりも集合として大きい、すなわち取り得る値の数が多いので、string
を返す関数のつもりでObject
を返す関数を扱ってしまうと、想定外の結果が返ってしまうことがあるのです。
以上の理由により、Covariant2<Object>
はCovariant2<string>
のスーパータイプである、言い換えると、Covariant2<Object>
の代わりとしてCovariant2<string>
は利用できるが、その逆、Covariant2<string>
の代わりとしてCovariant2<Object>
を利用することはできない、と言えます。
反変: 関数の引数として受け取る場合
「反変」は、「反」という字が含まれていることから察せられる通り、「共変」とは反対の方向に振る舞います。例を見てみましょう:
interface Contravariant<T> {
receiveValue: (value: T) => void;
}
Contravariant
型は、型引数T
に対して反変です。receiveValue
プロパティーのように、型引数の型を関数の引数とする関数を含んでいる場合、対象の型引数に対して反変になります。反変であるということは、Contravariant
に適当な型を渡して作った型同士 --- 例えばContravariant<string>
やContravariant<Object>
--- のサブタイプ・スーパータイプの関係を、渡した型同士の関係から反対にすることを表します。
例えばObject
はstring
のスーパータイプなので、それぞれをContravariant
に渡したContravariant<string>
とContravariant<Object>
におけるサブタイプ・スーパータイプの関係はその逆、すなわちContravariant<string>
がContravariant<Object>
のスーパータイプ、ということになります。
コードを書いてtscに聞いてみても、確かにそうなるようです:
const contra: Contravariant<string> =
{ receiveValue: (_value: Object) => {} };
const contraBad: Contravariant<Object> =
{ receiveValue: (_value: string) => {} };
// 型エラー: Type '(_value: string) => void' is not assignable to type '(value: Object) => void'.
// Types of parameters '_value' and 'value' are incompatible.
// Type 'Object' is not assignable to type 'string'. [2322]
ここまでのコードをTypeScript Playgroundで試す
なぜ関係が逆になるという、一見直感に反する結果となるのでしょうか?ここでも、関数を筒のようなもので例えて説明します。
今度は関数の引数が型引数の型で置き換えられるので、筒の左側の部分に型を置きました。
こちらも実際にContravariant<Object>
のような、Object
型の値を受け取る関数を持ったオブジェクトを使う立場に立って考えます。Contravariant<Object>
、つまりObject
型の値を受け取る関数を含んだオブジェクトのつもりで、Contravariant<string>
、つまりObject
型よりも「小さい」値のみを受け取る関数を持ったオブジェクトを扱うと、次のような問題が起こり得るからです:
図のように、Contravariant<Object>
のつもりでContravariant<string>
に含まれている関数を使用すると、string
には含まれないけどObject
には含まれる型 --- 例えばnumber
--- の値をうっかり渡してしまう恐れがあります。Contravariant<string>
の関数はstring
のことしか想定していないので、それ以外の値を渡すと当然問題があります。したがって、Contravariant<string>
をContravariant<Object>
の代わりに用いるのは型エラーとなります。
まとめ
反変・共変とは:
- ある型が型引数の型に対して共変である場合は、渡した型引数間のサブタイプ・スーパータイプの関係を変えない
- ある型が型引数の型に対して反変である場合は、渡した型引数間のサブタイプ・スーパータイプの関係を反対にする
どういう型が、ある型引数に対して反変・共変になるか:
- 型引数の型が、関数の戻り値(あるいは関数でない、単純なプロパティー)として使われていれば共変となり、
- 型引数の型が、関数の引数として使われていれば反変となる
この違いは、関数を使う側と関数自身、どちらが変数(引数)に代入する値を決める立場にあるか、という点にあります。関数の戻り値は、関数自身が変数に代入する値を決める一方、関数の引数は、関数を使う側が引数に代入する値を決めるからです。変数に代入する値の型は、変数の型のサブタイプでなければならないので、代入する値の型(引数として渡す型)がサブタイプとなるように定められているのです。
参考文献(本文中で言及しなかったもののみ)
- プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで
- サブタイピング (計算機科学) - Wikipedia
- Covariance and contravariance (computer science) - Wikipedia
-
TypeScriptを含め、昨今のプログラミング言語では推論された式の型に合わせて変数の型が決まる場合の方が多いですが、この記事では説明しやすさのために、全ての変数に予め型を設定しておきます。 ↩︎
Discussion