💨

共変性・反変性とは型構築子が部分型関係をどう保ち、どう変換するかという性質のことである

2022/12/02に公開約3,600字

まえがき

  • プログラミングに役立つと筆者が考える範囲で、プログラミングにおける共変性・反変性など(まとめて変性と呼びます)について解説をします。
  • 圏論的な視点の話は、筆者が詳しくなく、さらに一般のプログラミングで重要になることが少なそうなので、今回は省略させていただきます。

先に理解しておく必要がある概念

共変性・反変性について理解するためには「部分型」の概念と、「型構築子」の概念の理解が必要になります。この章ではこの2概念を確認していきます。

部分型(サブタイプ)

「部分型である」/「部分型でない」とは、2つの型の間の関係です。
T2型の値が期待されている箇所で、T2型の値の代わりにT1型の値を渡して問題がないとき、T1T2部分型(サブタイプ) であるといいます。T1T2の部分型であることはよく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)なので今回の例では明示的に継承することが必要になっています。)
したがって今回 WorkerHumanの部分型でありWorker <: Humanです。

型構築子

型構築子とは、型を受け取って新しい型を作るものを指します。多くの言語でのジェネリクス/ジェネリッククラスに相当します。
C# の例では、たとえばList<T>は型構築子だとみなすことができます。
Listは、intstringbool などの具体的な型を受け取って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#などでは、潜在的に共変や反変に扱いうる場合でも、デフォルトでは不変な型と同様に扱っています。
潜在的に共変・反変なジェネリクスクラスについては、型引数にinoutなどのキーワードを追加することで、明示的に共変、反変であるような扱いを許可するようにしています。

まとめ

  • 反変・共変などの変性は、具体的な型ではなく、型構築子(or ジェネリッククラス or ...)に対して性質を述べる述語です
  • 変性とは、型構築子が部分型関係をどう言う形で維持するか/しないかについての性質です
GitHubで編集を提案

Discussion

ログインするとコメントできます