共変性・反変性とは型構築子が部分型関係をどう保ち、どう変換するかという性質のことである
まえがき
- プログラミングに役立つと筆者が考える範囲で、プログラミングにおける共変性・反変性など(まとめて変性と呼びます)について解説をします。
- 圏論的な視点の話は、筆者が詳しくなく、さらに一般のプログラミングで重要になることが少なそうなので、今回は省略させていただきます。
先に理解しておく必要がある概念
共変性・反変性について理解するためには「部分型」の概念と、「型構築子」の概念の理解が必要になります。この章ではこの2概念を確認していきます。
部分型(サブタイプ)
「部分型である」/「部分型でない」とは、2つの型の間の関係です。
T2
型の値が期待されている箇所で、T2
型の値の代わりにT1
型の値を渡して問題がないとき、T1
はT2
の部分型(サブタイプ) であるといいます。T1
がT2
の部分型であることはよくT1 <: T2
のように表されます。
C#のコードで例を見てみます。下はHuman
クラスと、それを継承したWorker
クラスの例です。
class Human
{
public string? Name { get; init; }
public string IntroduceThemself() => $"My name is {Name}.";
}
class Worker : Human
{
public string? Job { get; init; }
public string IntroduceJob() => $"{Job} is an interesting job.";
}
以下のように、Human
型の値が期待されているところで、代わりにWorker
型の値を渡すことができます。
Human human = new Worker { Name = "Tom", Job = "programmer" };
Console.WriteLine(human.IntroduceThemself());
// "My name is Tom." と表示される
Human
型の値にはIntroduceThemself
メソッドを呼び出せることが期待されています。Worker
型の値もIntroduceThemself
メソッドを持つので、Human
型の値の代わりにWorker
型の値を渡しても原理的には問題なくうまく動くはずです。(追加の制約として、C#は公称的型付け(nominal typing)なので今回の例では明示的に継承することが必要になっています。)
したがって今回 Worker
はHuman
の部分型でありWorker <: Human
です。
型構築子
型構築子とは、型を受け取って新しい型を作るものを指します。多くの言語でのジェネリクス/ジェネリッククラスに相当します。
C# の例では、たとえばList<T>
は型構築子だとみなすことができます。
List
は、int
、 string
、 bool
などの具体的な型を受け取ってList<int>
、 List<string>
、List<bool>
などの新しい別の型を作ります。
4種の変性とは何か
以上の前置きを踏まえたうえで各変性の定義を確認します。
共変の定義
A <: B
ならば T<A> <: T<B>
のとき、型構築子T
は共変である
反変の定義
A <: B
ならば T<A> >: T<B>
のとき、型構築子T
は反変である
双変の定義
型構築子T
が共変かつ反変であるとき、型構築子T
は双変である
不変の定義
- 型構築子
T
が共変でも反変でもないとき、型構築子T
は不変である
以下の章で実例を確認していきます。
それぞれの変性を持つ型構築子の例
C#風の疑似コードで説明します。
共変の型構築子の例
以下のReader
型構築子は共変です。
class Reader<T>(){
T Read() {
...
}
}
A <: B
のとき、以下のようにReader<B>
型の値が欲しい場所で代わりにReader<A>
の値を与えても問題は起きません。
Reader<B> readerB = new ReaderA();
B bValue = readerB.read(); // bValueに実質A型の値を入れようとしているのでOK
つまり、 Reader<A> <: Reader<B>
なのでReader
は共変です。
しかし以下のように、Reader<A>
が欲しい場所で代わりにReader<B>
型の値を使うことはできません。
Reader<A> readerA = new ReaderB();
A aValue = readerA.read(); // aValueに実質B型の値を入れようとしていてまずい
つまりReader<A> >: Reader<B>
ではないためReader
は反変ではありません。
反変の型構築子の例
以下のWriter
型構築子は反変です。
class Writer<T> {
void Write(T value) {
...
}
}
A <: B
のとき、以下のようにWriter<B>
型の値が欲しい場所で代わりにWriter<A>
の値を与えることはできません。
B bValue = ...;
Writer<B> writerB = new WriterA();
writerB.write(bValue); // A型の値が欲しいwriteメソッドにB型の値を渡していてまずい
つまり Writer<A> <: Writer<B>
ではないためWriter
は共変ではありません。
しかし、以下で確認するようにWriter<A>
型の値が欲しい場所で代わりにWriter<B>
を渡すことはできます。
A aValue = ...;
Writer<A> writerA = new WriterB();
writerA.write(aValue) // B型の値が欲しいwriteメソッドにA型の値を渡しているのでOK
つまり Writer<A> >: Writer<B>
であるためWriter
は反変です。
不変の型構築子の例
以下の ReadWriter
は不変です。
class ReadWriter<T> {
T Read() {
...
}
void Write(T value) {
..
}
}
A <: B
のとき、上で見た例のように、
-
ReadWriter<B>
型の値が欲しいところでReadWriter<A>
型の値を渡すことはできません。よってReadWriter
は共変ではありません。 -
ReadWriter<A>
型の値が欲しいところでReadWriter<B>
型の値を渡すことはできません。よってReadWriter
は反変ではありません。
双変の型構築子の例
次の RandomSleeper
は双変です。
class RandomSleeper<T> {
void sleepRandomSeconds() {
...
}
}
T
を本質的に使っている場所がどこにもないためです。
実際のプログラミング言語における事例
C#などでは、潜在的に共変や反変に扱いうる場合でも、デフォルトでは不変な型と同様に扱っています。
潜在的に共変・反変なジェネリクスクラスについては、型引数にin
やout
などのキーワードを追加することで、明示的に共変、反変であるような扱いを許可するようにしています。
まとめ
- 反変・共変などの変性は、具体的な型ではなく、型構築子(or ジェネリッククラス or ...)に対して性質を述べる述語です
- 変性とは、型構築子が部分型関係をどう言う形で維持するか/しないかについての性質です
Discussion