ポリモーフィズム
2021-10
ポリモーフィズム(polymorphism)とは、それぞれ異なる型に一元アクセスできる共通接点の提供、またはそれぞれ異なる型の多重定義を一括表現できる共通記号の提供を目的にした、型理論またはプログラミング言語理論の概念および実装である。この用語は、有機組織および生物の種は様々な形態と段階を持つという生物学からの借用語である。多態性、多相性と邦訳されることが多い。
ポリモーフィズムは、通常以下の三種に分けられる。
アドホック多相(ad hoc polymorphism)- 恣意的な型の集合に一つの共通接点を提供する。関数オーバーロード、Mix-inの一実装、型クラスなど。
パラメトリック多相(parametric polymorphism)- 詳細化されていない型要素を内包する抽象的な型に記号表現を提供する。ジェネリクスや関数型言語の型構築子など。
サブタイピング(subtyping)- サブタイプ多相(subtype polymorphism)やインクルージョン多相(inclusion polymorphism)とも。上位型をその下位型の数々で代替できるようにする。オブジェクト指向の多態性はこれを指す。
この他に、ロー多相(row polymorphism)とポリタイピズム(polytypism)も挙げられることがある。対義語はモノモーフィズム(Monomorphism)である。
ポリモーフィックな型システムの研究は1960年代から始められている。クリストファー・ストレイチーの1967年論文Fundamental Concepts in Programming Languagesで、アドホック多相とパラメトリック多相という概念が初めて提唱されている。アドホック多相は「ALGOL68」で実践され、パラメトリック多相は「ML」の型システムで実践された。 1985年にピーター・ウェグナーとルカ・カーデリは「Simula67」の継承+動的ディスパッチを説明するためのインクルージョン多相という概念を提唱した。これとストレイチー提唱の二つを合わせて三本柱にした概念が、ポリモーフィズムと呼ばれるようになっている。
1989年にオブジェクト指向の動的型付けを説明するためのロー多相という概念がミッチェル・ワンドの著作で提唱されているが、知名度は低い。同年にパラメトリック多相をモチーフにしたジェネリックプログラミングがアレクサンダー・ステパノフらの著作で提唱され、これはポリタイピズムとも呼ばれた。
1980年代の「Ada」はジェネリクスに型制約と称する有界量化の概念を採用した。1990年代の「Haskell」はアドホック多相とジェネリクスを融合した型クラスを考案している。2003年の「Scala」はサブタイピング+ジェネリクスに共変性と反変性を導入した。
関数や演算子の多重定義のように、同じ名前で型の異なる引数に適用できて、その振る舞いは引数の型によって違うような関数の多相性のことを「アドホック多相」という。「ad hoc(その場しのぎの)」という用語は悪い意味で使われているのではなく、単にこの種の多相性が型システムの基本的な機能ではないという事実を指して使われている。次のC++での例では、Add関数は呼び出し側からは様々な型に対して総称的に動作するかのように見えるが、コンパイラから見ればこれらは全く別個の2つの関数である。
動的型付け言語では、実行されるべき正しい関数が実行時まで決定できない可能性があるという点で、状況はより複雑になりうる。暗黙の型変換も型強制多相(coercion polymorphism)としてアドホック多相の一形態と定義される。
パラメトリック多相を使うと、値の型に関係なく「一様に」値を扱うことで、関数やデータ型を総称的に記述できるようになる。パラメトリック多相は言語の静的な型安全性を保ちながら表現力を向上させる手法のひとつである。
パラメトリック多相の概念は関数とデータ型の両方に適用される。異なる型の値に対して評価、適用可能な関数のことを「多相な関数」という。総称化された型とみなすことができるデータ型(例えば任意の型の要素を持てるリスト)は「多相なデータ型」という。
パラメトリック多相性は関数型プログラミングの分野では至るところに現れるため、しばしば単に「多相性」と言われることがある。次のHaskellの例ではパラメータ化されたリストと2つのパラメトリック多相な関数を示す。
パラメトリック多相は様々なオブジェクト指向言語でも利用できる。例えばC++やD言語のテンプレート、JavaやC#のジェネリクスなどである。
ジャン=イヴ・ジラールとJohn C. Reynoldsは、それぞれ独立に、パラメトリック多相の概念をラムダ計算の拡張(System Fや多相ラムダ計算と呼ばれる)として形式的に発展させた。
いくつかのプログラミング言語では、特定の多相性の状況において使用できる型の範囲を制限するためにサブタイピングを採用している。サブタイピングを使用すると、ある型Tのオブジェクトを受け取る関数は、Tのサブタイプである型Sのオブジェクトを渡された場合でも正しく動作する(リスコフの置換原則)。この型の関係性はしばしばS <: Tと表記される。一般的にサブタイプ多相=インクルージョン多相は動的に解決される(後述)。
次のJavaの例ではAnimalのサブタイプとしてCatとDogを用意する。メソッドletsHear()はAnimal型の引数を受け取るが、そのサブタイプの引数を渡しても問題なく動作する。
オブジェクト指向言語は継承によってサブタイピングを提供する。典型的な実装では、各クラスはそれぞれ仮想関数テーブル(vtable)と呼ばれる関数のテーブルを持ち、各オブジェクトは自らのクラスのvtableへのポインタを持つ。多相なメソッドを呼び出すときには、このvtableを参照する。
多くのオブジェクト指向言語では、仮想関数の呼び出しに1番目の引数(thisオブジェクト)の vtable だけを参照する単一ディスパッチを採用している。つまりその他の引数の実行時の型は仮想関数の呼び出しに全く無関係である。一方でCommon Lispなどでは、メソッドの呼び出しが「全ての」引数に対して多相的となる多重ディスパッチを採用している。
ロー多相は、型理論におけるレコード型(record type)の直積的または総和的な可変長要素の構造分析から導き出された多態性であり、動的型付けおよび動的プログラミングを説明できる形式論理として紹介される。構造的型付け (structural typing) の多態性(多相性)はロー多相に分類されることがある。構造的型付けに類似したダックタイピングの説明にも適している。
日本語では行多相と訳されることもあれば、列多相と訳されることもある。
ポリタイピズムは、パラメトリック多相の亜流と言えるものである。パラメトリック多相での型が型変数を内包するという概念を、ポリタイピズムでは型が包装型を着脱するという概念に置き換えている。包装型=コンテナである。コンテナの着脱は圏論での関手に類似している。ポリタイピズムは、ジェネリックプログラミングを説明する多態性として扱われている。
ポリモーフィズムは実装がいつ選択されるかによって、静的(コンパイル時)か動的(実行時)かに区別できる。これはそれぞれ静的ディスパッチおよび動的ディスパッチとして知られ、さらにこれらに対応するポリモーフィズムはそれぞれ静的ポリモーフィズムおよび動的ポリモーフィズムと呼ばれる。後者は典型的には仮想関数などを通して実現される。
動的ディスパッチのオーバーヘッドが無いため、静的ポリモーフィズムはより高速に実行できるが、追加的なコンパイラの補助を必要とする。静的ポリモーフィズムではコンパイラやソースコード解析ツール、プログラマの目視による、より広範な静的解析(特に最適化)が可能となる。動的ポリモーフィズムはより柔軟だが速度はより遅くなる。例えば動的ディスパッチではダック・タイピングが可能で、動的にリンクされたライブラリはオブジェクトの型を知らなくても動作できるだろう。
典型的にはアドホック多相とパラメトリック多相は静的ポリモーフィズムとして動作し、サブタイプ多相は動的ポリモーフィズムとして動作する。しかし、奇妙に再帰したテンプレートパターンのような洗練されたテンプレートメタプログラミングを通して、サブタイプ多相で静的ポリモーフィズムを実現することも可能である。
ポリモーフィズムの対義語としてモノモーフィズム(monomorphism、単態性、単相性)という言葉が使われることがある。
モノモーフィックな型システムを持つプログラミング言語では、サブルーチン(言語によっては関数や手続きとも呼ばれる)はそれぞれ一意に識別される名前(識別子)と結びついており、従って異なる動作を実現するためには異なる名前を用いる必要があった。
例えば、何かの値を文字列形式に変換する最も単純な場合を考える。モノモーフィックな型システムを持つ言語では、次のように別々の関数になっていなければならない。
古典的な変換関数:
string = StringFromNumber(number)
string = StringFromDate(date)
一方ポリモーフィックな型システムを持つ言語では、StringValue のような汎用の述語を定義し、型別にそれぞれ適切な変換方式を定義させることでオブジェクトの種別によらない抽象度の高い変換形式を実現できる。
多態的な変換方式:
見た目上、型によらない変換が可能
string = number.StringValue
string = date.StringValue
関数オーバーロードをサポートする言語では、共通の名前を持ち、引数の型だけが異なる StringFrom のような関数を定義することもできる。
string = StringFrom(number)
string = StringFrom(date)
無論、StringValueやStringFromの定義は型ごとに行なわなければならないので、総体として記述量が減少するとは限らない(継承やジェネリックプログラミングによるコードの再利用はありうる)。また、何をもって「正しい動作」とするのかはオブジェクトの設計に依存するため、多態を使いこなすにはシステム全体を見通す設計能力が要求される。