「ジェネリックプログラミング」の版間の差分
問題部分を削除して再掲載しました タグ: 差し戻し済み サイズの大幅な増減 ビジュアルエディター |
タグ: 取り消し サイズの大幅な増減 |
||
1行目: | 1行目: | ||
'''ジェネリック'''(総称あるいは汎用)'''プログラミング'''({{lang-en-short|generic programming}})は、具体的なデータ型に直接依存しない、抽象的かつ汎用的なコード記述を可能にする[[プログラミング (コンピュータ)|コンピュータプログラミング]]手法である。 |
|||
[[ファイル:Symbol template class.svg|境界|右|フレームなし|206x206ピクセル]] |
|||
{{プログラミング・パラダイム}} |
|||
== 概要 == |
|||
'''ジェネリックプログラミング'''({{lang-en-short|''generic programming''}})は、[[プログラミング|コンピュータプログラミング]]のための手法またはスタイルの一つである。特定の[[データ型]]に依存しないプログラム記述を目的にしており、これを型(''type'')の総称化(''generic'')と呼ぶ。静的な機能であり、総称化された型は[[コンパイラ|コンパイル]]時に特有化(''specific'')決定される。[[クラス (コンピュータ)|クラス]]定義、[[モジュール]]定義、[[関数 (プログラミング)|関数]]定義のコード記述箇所で任意の型が総称化され、[[クラス (コンピュータ)|クラス]]の[[インスタンス化]]、[[モジュール]]のメモリ展開化、[[関数 (プログラミング)|関数]]呼び出しのコード記述箇所で特有化される。型差分の重複コードを削減できるのが主な利点である。[[プログラミング言語]]組込の機能名としては、ジェネリクス(''generics'')やテンプレート(''template'')などと呼ばれる。 |
|||
ジェネリックプログラミングは[[データ型]]でコードを[[インスタンス]]化するのか、あるいはデータ型をパラメータとして渡すかということにかかわらず、同じソースコードを利用できる<ref>{{Cite_web|url=http://msdn.microsoft.com/msdnmag/issues/05/04/PureC/|title=Pure C++:Generic Programming Under .NET|author=Stanley B. Lippman|publisher=[[マイクロソフト]]・[[MSDN]]マガジン|accessdate=2008-12-28|deadlinkdate=2019-03}}</ref>。ジェネリックプログラミングは言語により異なる形で実装されている。ジェネリックプログラミングの機能は1970年代に[[CLU]]や[[Ada]]のような言語に搭載され、次に[[BETA]]、[[C++]]、[[D言語|D]]、[[Eiffel]]、[[Java]]、その後[[ディジタル・イクイップメント・コーポレーション|DEC]]の[[Trellis/Owl]]言語などの数多くのオブジェクトベース (object-based) および[[オブジェクト指向]] (object-oriented) 言語に採用された。 |
|||
1995年の書籍[[デザインパターン (ソフトウェア)|デザインパターン]]{{Full|date=2019年3月}}の共著者{{誰|date=2019年3月}}は(Ada、Eiffel、Java、[[C Sharp|C#]]の)ジェネリクスや、(C++の)[[テンプレート (プログラミング)|テンプレート]]としても知られるパラメータ化された型 (parameterized types) としてジェネリクスについて触れている。これらは、型を指定することなく、型を定義できるようにする(型は使用する時点で引数として与えられる)。このテクニック(特に[[デリゲート (プログラミング)|デリゲーション]]を組み合わせるとき)は非常に強力である。 |
|||
このスタイルのルーツは、1973年の[[関数型言語]]「[[ML (プログラミング言語)|ML]]」の{{仮リンク|パラメトリック多相|en|Parametric polymorphism|label=}}であり、1983年の「[[Ada]]」のジェネリクス、1986年の「[[Eiffel]]」のジェネリシティ、1986年の「[[C++]]」のテンプレート導入を経て、1989年に計算機科学者{{仮リンク|デビッド・マッサー|en|David Musser|label=}}と{{仮リンク|アレクサンダー・ステパノフ|en|Alexander Stepanov|label=}}の両人が、正式にジェネリックプログラミングの名義で確立した。以降、[[Standard ML|SML]]・[[Haskell]]・[[Scala]]・[[Java]]・[[C Sharp|C#]]・[[D言語|D]]・[[Delphi]]・[[Visual Basic|VB.net]]・[[F Sharp|F#]]・[[Rust (プログラミング言語)|Rust]]・[[TypeScript]]・[[Python]]・[[Swift (プログラミング言語)|Swift]]などの多数の言語仕様に採用されている。 |
|||
== 特徴 == |
== 特徴 == |
||
ジェネリックプログラミングの特徴は、型を抽象化してコードの再利用性を向上させつつ、[[静的型付け]]言語の持つ型安全性を維持できることである。 |
|||
ジェネリックとは、[[データ型]]を任意の抽象記号にした[[ソースコード]]記述をプログラマに許可し、[[コンパイラ]]が一定の指定構文に従い、抽象記号箇所に適切なデータ型を自動的に当てはめて、[[中間表現|中間表現コード]]を生成する仕組みを意味する。端的に言うとコンパイラによる[[型安全]]が保証された[[マクロ (コンピュータ用語)|パラメータ付きマクロ]]と考えてもよい。その最大の利点は、対象データ型の部分だけが異なる重複コードをひとまとめにできる差分プログラミング的な効用である。例としては<code>double atof(char* c)</code> <code>int atoi(char* c)</code> <code>long atol(char* c)</code>といった返り値の型が違うだけのコードをその型を総称化した<code>T ato<T>(char* c)</code>にまとめられる。ジェネリックの実装には標準方式、後付けジェネリック方式、テンプレートメタ方式、関数型言語方式の四つがあり、特有化インスタンス・型安全・型キャストの各性質に微妙な違いがある。 |
|||
ジェネリックプログラミングを用いない場合、例えば伝統的な[[C言語]]や[[Pascal]]のような従来の[[静的型付け]]言語において、ソートなどのアルゴリズムや[[連結リスト]]のようなデータ構造(オブジェクトの[[コンテナ (データ型)|コンテナ]])を記述する際は、たとえ対象となる要素のデータ型が異なるだけで事実上同一のコードであったとしても、具体的なデータ型ごとにそれぞれ実装しなければならない。[[整数型]]のリスト、[[倍精度浮動小数点数]]型のリスト、[[文字列]]型のリスト、ユーザー定義[[構造体]]のリスト、……といった具合である。もしジェネリックプログラミングをサポートしない言語で汎用的なコードを記述して再利用しようと思えば、メモリ空間効率や型安全性などを犠牲にしなければならなくなる([[共用体]]や汎用[[ポインタ (プログラミング)|ポインタ]]型と[[型変換|キャスト]]を駆使するなど)。一方、C++の関数テンプレートやクラステンプレートのように、ジェネリックプログラミングを用いることで、抽象化された型について一度だけ記述したアルゴリズムやデータ構造をさまざまな具象データ型に適用して、コードを型安全に再利用できるようになる。これがジェネリックプログラミングの利点の一例として挙げられる。 |
|||
=== ジェネリッククラス === |
|||
ジェネリッククラスは、その任意のメンバ変数の型、メンバ関数の引数/返り値の型、メンバ関数内のローカル変数の型を総称化できるクラス定義である。総称化された型は一般的に型パラメータ(''type parameter'')と呼ばれる。プレースホルダ(''place holder'')とも呼ばれる。型パラメータの宣言は<code><T></code> <code>[T, U]</code> <code>template<typename T></code>のように大抵は角括弧が用いられてクラス名の隣に付記される。ジェネリッククラスのインスタンス化は、スペシフィックインスタンス化と呼ばれる。スペシフィックインスタンス化ではコンストラクタの隣に<code><int></code> <code>[int]</code>のように当てはめ型(''applying type'')を付記する。ここでは最も構文が平易なC#を例にする。 |
|||
<syntaxhighlight lang="csharp"> |
|||
public class GenTest<T> { |
|||
private T mT; |
|||
public GenTest(T pT) { |
|||
mT = pT; |
|||
} |
|||
} |
|||
以下にC++の例を示す。 |
|||
GenTest<int> g1 = new GenTest<int>(1); // int型でスペシフィックインスタンス化 |
|||
</syntaxhighlight> |
|||
=== ジェネリック関数 === |
|||
ジェネリック関数は、その引数/返り値の型、ローカル変数の型を総称化できる関数定義である。型パラメータの宣言は<code><T, U></code> <code>[T]</code> <code>template<typename T></code>のように大抵は角括弧が用いられて関数名の隣に付記される。ジェネリック関数の呼び出しは、スペシフィックコールと呼ばれる。スペシフィックコールでは、引数が総称化対象ならば型推論によって関数呼び出し記述の実引数値がそのまま当てはめ型の指定になる。返り値とローカル変数が総称化対象ならば関数名の隣に<code><int></code> <code>[int]</code>のように当てはめ型を付記する。ここではジェネリック関数が最も多用されるC++を例にする。 |
|||
<syntaxhighlight lang="cpp"> |
|||
template <typename T> |
|||
T max(T x, T y) { |
|||
return x < y ? y : x; |
|||
} |
|||
std::cout << max(3, 7); // int型でmaxをスペシフィックコール |
|||
</syntaxhighlight> |
|||
=== パラメータ制約 === |
|||
制約(''constraint'')とは、型パラメータに当てはめられる型を、指定した型の派生型に限定する機能を指す。指定したクラスのサブクラスに限定する機能とも言い換えられる。型パラメータはその指定した型またはクラスで記号修飾されて宣言される。制約方式は、スーパークラスを指定してその派生クラスグループに限定するものと、値型/参照型/ポインタ型/数値型/実数型/配列型/構造体型/モジュール型/クラス型/インターフェース型/といったデータ性質を指定してそれに限定するものの二通りがある。型パラメータに当てはめる型が制約(''constraint'')外ならコンパイルエラーになる。これによって不正な当てはめ型を静的にチェックできるようになる。 |
|||
=== イテレータ === |
|||
イテレータは反復子とも邦訳され、ジェネリック化されたコレクションクラス(Array、List、Set、Mapなど)の収納要素を取り出す機能である。 |
|||
=== 共変性と反変性 === |
|||
共変性(''covariance'')とは、当てはめ型の継承関係をそのままジェネリッククラスの継承関係にシフトさせる機能を指している。反変性(''contravariance'')は、当てはめ型の継承関係を逆にしてシフトさせる。ここで<code>List<T></code>をジェネリックラスとし、<code>猫</code>クラスを<code>動物</code>クラスのサブクラスとすると、共変性では<code>List<猫></code>は<code>List<動物></code>のサブクラスになる。反変性では<code>List<動物></code>は<code>List<猫></code>のサブクラスになる。 |
|||
=== ジェネリック実装方式の違い === |
|||
== 来歴 == |
|||
マッサーとステパノフは1989年の著作でジェネリックプログラミングをこのように定義している。 |
|||
ジェネリックプログラミングは定型コードの抽象化に焦点を当てている。それは異なるデータ表現を結合する総称化アルゴリズムを獲得して、ソフトウェアの拡張を促進する。 |
|||
このパラダイムの原点は、1973年公開の[[関数型言語]]「[[ML (プログラミング言語)|ML]]」の{{仮リンク|パラメトリック多相|en|Parametric polymorphism|label=}}に求めることができる。これは[[型理論]]([[型付きラムダ計算]])に沿って、型の合成体としての型を扱うための仕組みであった。合成内の直積や直和といった構造パターンはデータ構築子で定義され、その構造パターン内の型要素を[[抽象代数学|抽象代数]](型変数)にしてその特有化は型構築子で定義される。型構築子に適用する型引数を変えることで一つの構造パターンを軸にした多様な型が生成される。その型付け値は引数として関数に適用され、パターンマッチングで抽象代数部分をその都度特定した。型構築子の考え方は、そのまま配列/構造体/データコンテナ/モジュールに応用できるものであった。しかしパラメトリック多相では型構築子の内部は総称化されるが、型構築子そのものを総称化するのは[[型付きラムダ計算]]理論の枠外になるので関数の引数/返り値を直接抽象代数にすることは出来なかった。この解決策として、型構築子(''type constrcutor'')を総称化子(''generics'')に置き換えたことが、パラメトリック多相とジェネリックプログラミングの分水嶺になっている。総称化子は1983年公開の[[手続き型言語]]「[[Ada]]」で初めて実装された。総称化子はパッケージ([[モジュール]])内の任意要素のデータ型をそのまま抽象記号化できて型安全が保証される[[マクロ (コンピュータ用語)|マクロ]]に似た仕組みになり、これはプログラミング目的に特化された機能と言えた。ステパノフのこのように述べている。 |
|||
ジェネリックプログラミングはアルゴリズムとデータ構造を抽象化して分類する。このアイディアは[[ドナルド・クヌース|クヌース]]([[文芸的プログラミング]]などの提唱者)由来であって[[型理論]]ではない。 |
|||
==Adaのジェネリクス== |
|||
1983年公開のAdaは、最初からジェネリクスを言語仕様に組み込んだ[[手続き型言語]]であった。ここではオブジェクト指向が導入される1995年以前のAdaのジェネリクスを対象にする。Adaは、データと手続きをまとめた[[モジュール]]をパッケージと呼んでおり、それを総称化したジェネリックパッケージを扱っている。パッケージ定義の冒頭でメンバ定数、メンバ変数の型、メンバ手続きの引数/返り値の型、メンバパッケージなどを型パラメータで総称化宣言できる。型パラメータは制約(''constraint'')で修飾できる。型パラメータには未指定時のデフォルト型も指定できる。ジェネリックパッケージの定義は以下のようになる。<syntaxhighlight lang="ada"> |
|||
generic |
|||
Max_Size : Natural; -- 総称化で用いられる形式値 |
|||
type Element_Type is private; -- Element_Typeをlimited型以外OKの制約で総称型に |
|||
package Stacks is -- Stackをジェネリックパッケージ宣言 |
|||
type Size_Type is range 0 .. Max_Size; |
|||
type Stack is limited private; |
|||
procedure Create (S : out Stack; |
|||
Initial_Size : in Size_Type := Max_Size); |
|||
procedure Push (Into : in out Stack; Element : in Element_Type); |
|||
procedure Pop (From : in out Stack; Element : out Element_Type); |
|||
Overflow : exception; |
|||
Underflow : exception; |
|||
private |
|||
subtype Index_Type is Size_Type range 1 .. Max_Size; |
|||
type Vector is array (Index_Type range <>) of Element_Type; |
|||
type Stack (Allocated_Size : Size_Type := 0) is record |
|||
Top : Index_Type; |
|||
Storage : Vector (1 .. Allocated_Size); |
|||
end record; |
|||
end Stacks; |
|||
</syntaxhighlight> |
|||
ジェネリックパッケージのスペシフィックインスタンス化は以下のようになる。 |
|||
<syntaxhighlight lang="ada"> |
|||
package Bookmark_Stacks is new Stacks (Max_Size => 20, |
|||
Element_Type => Bookmark_Type); |
|||
</syntaxhighlight> |
|||
そのスペシフィックインスタンスの使用は以下のようになる。 |
|||
<syntaxhighlight lang="ada"> |
|||
type Document_Type is record |
|||
Contents : Ada.Strings.Unbounded.Unbounded_String; |
|||
Bookmarks : Bookmark_Stacks.Stack; |
|||
end record; |
|||
procedure Edit (Document_Name : in String) is |
|||
Document : Document_Type; |
|||
begin |
|||
Bookmark_Stacks.Create (S => Document.Bookmarks, Initial_Size => 10); |
|||
end Edit; |
|||
</syntaxhighlight>型パラメータは制約(''constraint'')で修飾できる。データ型性質の[[上位概念、下位概念、同位概念および同一概念|上位概念]]が「制約」になり、離散型、実数型、配列型、パッケージ型、アクセス型([[ポインタ (プログラミング)|ポインタ]])などで当てはめ型を制約できる。制約もまた総称化できるので、総称化されたデータ型性質で制約を課すこともできる。その例は以下のようになる。 |
|||
<syntaxhighlight lang="ada"> |
|||
generic |
|||
type Index_Type is (<>); -- discrete型のみOK |
|||
type Element_Type is private; -- limited型以外OK |
|||
type Array_Type is array (Index_Type range <>) of Element_Type; |
|||
</syntaxhighlight> |
|||
この例でArray_Typeには、Element_Type制約下のデータ型を要素とし、Index_Type制約下のデータ型を添字とする配列型であるという制約を課している。プログラマがこのジェネリックパッケージをスペシフィックインスタンス化するには、その制約を満たす配列型を型パラメータに当てはめねばならない。 |
|||
==Eiffelのジェネリシティ== |
|||
1986年公開のEiffelは、最初からジェネリシティを言語仕様に組み込んだオブジェクト指向言語である。クラスに総称化を取り入れた最も初期の言語である。ジェネリッククラスの定義は以下のようになる。 |
|||
<syntaxhighlight lang="Eiffel"> |
|||
class |
|||
LIST [G] |
|||
... |
|||
feature -- Access |
|||
item: G |
|||
-- The item currently pointed to by cursor |
|||
... |
|||
feature -- Element change |
|||
put (new_item: G) |
|||
-- Add `new_item' at the end of the list |
|||
... |
|||
</syntaxhighlight> |
|||
ジェネリッククラスのスペシフィックインスタンス化は以下のようになる。 |
|||
<syntaxhighlight lang="Eiffel"> |
|||
list_of_accounts: LIST [ACCOUNT] |
|||
-- Account list |
|||
list_of_deposits: LIST [DEPOSIT] |
|||
-- Deposit list |
|||
</syntaxhighlight>ジェネリッククラスの型パラメータは制約(''constraint'')で修飾できる。<syntaxhighlight lang="Eiffel"> |
|||
class |
|||
SORTED_LIST [G -> COMPARABLE] |
|||
</syntaxhighlight> |
|||
==C++のテンプレート== |
|||
{{main|テンプレート (プログラミング)}} |
|||
C++では1986年からテンプレート機能が導入された。C++のテンプレートは、ソースコード・トランスレータの仕組みに沿った[[テンプレートメタプログラミング]]方式の機能だったので、AdaやEiffelのジェネリックとは微妙な違いがあった。C++はテンプレートクラスとテンプレート関数をサポートし、C++11からテンプレートエイリアス、[[C++14]]からテンプレート変数も加えられた。パラメータ制約はない。1993年から発表された[[Standard Template Library|標準テンプレートライブラリ]] (STL) は、ジェネリックプログラミングの普及に最も貢献している。テンプレートクラスは以下のようになる。ここでは[[双方向連結リスト]]をテンプレートクラスにしている。その要素の型を<code>T</code>としている。<code><typename T></code>はテンプレートクラスのプレースホルダ(型パラメータ)の宣言である。 |
|||
<syntaxhighlight lang="cpp"> |
<syntaxhighlight lang="cpp"> |
||
217行目: | 89行目: | ||
--> |
--> |
||
上記は要素型を<code>T</code>とする[[双方向連結リスト]]の定義例である。<code>typename T</code>はテンプレートによる抽象化の対象となる型の名前(プレースホルダー)を表す。そしてこの定義されたクラステンプレートの'''インスタンス化'''、すなわち型パラメータ<code>T</code>に具象型を与えることによって生成されるクラス型は、<code>T</code>について実際に指定した具象型のリストとして扱われる。これらの「T型のコンテナ」を一般に'''ジェネリクス''' (generics) と呼び、ジェネリックプログラミングの代表的なテクニックである。プログラミング言語によって制約は様々だが、このテクニックは、[[継承 (プログラミング)|継承]]関係や[[シグネチャ]]といった制約条件 (constraint) を維持する限り、内包する<code>T</code>にあらゆるデータ型を指定可能なクラスの定義を可能にする。これはジェネリックプログラミングの典型であり、一部の言語{{要説明|date=2019年3月}}ではこの形式のみを実装する。ただし、概念としてのジェネリックプログラミングはジェネリクスに限定されない。 |
|||
テンプレート関数は以下のようになる。ここではスワップ関数をテンプレートにしている。 |
|||
オブジェクト指向プログラミング言語は、サブタイプ(派生型)でスーパータイプ(基底型)の振る舞い(アルゴリズム)を[[オーバーライド]]することによる動的な[[ポリモーフィズム]](多態性)を備えており、動的な多態性もまたスーパータイプによる抽象化とサブタイプによる具象化<ref>[[統一モデリング言語]] (UML) の用語では、それぞれ汎化 (generalization) および特化 (specialization) と呼ぶ。</ref>を実現するものだが、ジェネリクスは静的な多態性による抽象化と具象化を実現するという点で設計を異にする。 |
|||
ジェネリックプログラミングのもう一つの応用例として、型に依存しないスワップ関数の例を示す。 |
|||
<syntaxhighlight lang="cpp"> |
<syntaxhighlight lang="cpp"> |
||
234行目: | 110行目: | ||
</syntaxhighlight> |
</syntaxhighlight> |
||
上記の例で使用したC++の<code>template</code>文は、プログラマーや言語の開発者たちにこの概念を普及させたジェネリックプログラミングの例といわれている。この構文はジェネリックプログラミングの全ての概念に対応する。またD言語はC++のテンプレートを基に構文を単純化した完全なジェネリックの機能を提供する。JavaはJ2SE 5.0よりC++の文法に近いジェネリックプログラミングの機能を提供しており、ジェネリクス(「T型のコンテナ」)という、ジェネリックプログラミングの部分集合を実装する。 |
|||
==関数型言語のジェネリック== |
|||
C# 2.0、[[Microsoft Visual Basic .NET|Visual Basic .NET]] 2005 (VB 8.0) では、[[.NET Framework|Microsoft .NET Framework]] 2.0がサポートするジェネリクスを利用するための構文が追加された。[[ML (プログラミング言語)|ML]]ファミリーは{{仮リンク|パラメータ多相|en|Parametric polymorphism}} (parametric polymorphism) とファンクタと呼ばれるジェネリックモジュールを利用してのジェネリックプログラミングを推奨する。[[Haskell]]のタイプクラスのメカニズムもまたジェネリックプログラミングに対応する。 |
|||
=== Standard MLのファンクタ === |
|||
[[Standard ML]]で1988年頃に採用された[[テンプレートメタプログラミング|テンプレートメタ]]方式のジェネリックプログラミングの機能名はファンクタ(''functor'')と呼ばれた。 |
|||
[[Objective-C]]にあるような[[動的型付け]]を使い、必要に応じて注意深くコーディング規約を守れば{{要説明|date=2019年3月}}、ジェネリックプログラミングの技術を使う必要がなくなる。全てのオブジェクトを包括する汎用型があるためである。Javaもまたそうであるが、キャストが必要なので静的な型付けの統一性を乱してしまう。例えば、ジェネリクスをサポートしていなかった時代のJavaでは、{{Javadoc:SE|java/util|List}}のようなコレクションに格納できる要素型は{{Javadoc:SE|java/lang|Object}}のみであったため、要素取り出しの際には実際のサブクラス型への適切なキャストが必要だった。それに対し、ジェネリクスは静的な型付けについての利点を持ちながら動的な型付けの利点を完全ではないが得られる方法である。 |
|||
=== Haskellの型クラス === |
|||
1990年に初版公開された[[Haskell]]では、[[多重定義|関数オーバーロード]]および特定の型付け値を扱うための関数モジュールを、[[型推論]]の枠組み内で実装できるようにするためにジェネリックプログラミングが取り入れられた。静的な関数型言語のそれはパラメトリック多相と融合された[[型理論]]に沿ったものになり、[[Ada]]、[[Eiffel]]、[[C++]]などのジェネリックとは一線を画した方式になっている。[[Haskell]]のジェネリックの機能名は[[型クラス]](''type classes'')と呼ばれ、[[型理論]]で言う文脈(''context'')と組み合わせられたものになっている。 |
|||
==Adaのジェネリクス== |
|||
[[型クラス]]は、総称型の値(型パラメータ)を引数/返り値/計算値として扱えるジェネリック関数群の関数の型(''function type'')と式内容(''expressions'')をそれぞれ定義できる構文である。型クラスの宣言名はそのまま文脈シンボルになって型構築子に付加できるものになる。型構築子=値の型である。型構築子の定義構文で型クラス名を付加することがスペシフィックインスタンス化になり、型クラス構文で定義されたジェネリック関数群の関数の型と式内容の型パラメータ箇所にその型構築子を当てはめたものがコンパイル時に生成されることになる。加えてその型構築子のためのスペシフィック関数の式内容も個別定義できる。下記の例は[[二分木]]データ構造のパラメトリック多相化代数的データ型BinTreeの定義構文に、型クラス<code>Eq</code>&<code>Show</code>を付加して同時にスペシフィックインスタンス化している。なおパラメトリック多相化代数的データ型はこちらも「generalized algebraic datatype」と呼ばれているが、ジェネリックとは区別する必要がある。 |
|||
Adaには1977年-1980年の設計当初から汎用体 (generics) が存在する。標準ライブラリでも多くのサービスを実装するために汎用体を用いている。Ada2005では1998年に規格化されたC++の[[Standard Template Library]] (STL) の影響を受けた広範な汎用コンテナが標準ライブラリとして追加された。 |
|||
汎用体 (generic unit) とは、0または複数の汎用体仮パラメータ (generic formal parameters) を採るプログラム単位(パッケージまたは副プログラム)である。 |
|||
<pre> |
|||
<!-- en の"one or more"は誤り.仮パラメータを持たない汎用体も宣言できる.--> |
|||
data BinTree a = Leaf a | Node (BinTree a) a (Bintree a) |
|||
deriving (Eq, Show) |
|||
</pre> |
|||
汎用体仮パラメータとしては、オブジェクト(変数・定数)、データ型、副プログラム、パッケージ,さらには他の汎用体のインスタンスさえ指定することができる。汎用体仮パラメータのデータ型としては、離散 (discrete) 型、[[浮動小数点数]]型、[[固定小数点数]]型、アクセス([[ポインタ (プログラミング)|ポインタ]])型などを用いることができる。 |
|||
== Scalaのジェネリクス == |
|||
2003年に初公開されたScalaの開発者である情報工学者マーティン・オーダスキーは、[[型理論]]と関数型プログラミングを専攻分野にしていたのでそのジェネリクスにバリアンスと型境界を取り入れている。バリアンスは[[圏論]]の共変関手と反変関手由来のものであり、ジェネリッククラス/トレイトの共変性と反変性の機能である。型境界は[[型理論]]の境界的量化子(''bounded quantifier'')由来のものであり、パラメータ制約を上限型境界と下限型境界に派生させた機能である。 |
|||
汎用体をインスタンス化する際、プログラマは全ての仮パラメータに対応する実パラメータを指定する必要があるが、プログラマが明示的に全ての実パラメータを指定しなくても済むよう,仮パラメータにはデフォルトを指定することもできる。インスタンス化してしまえば,汎用体のインスタンスは、汎用体ではない通常のプログラム単位であるかのように振舞う。インスタンス化は実行時、例えばループの中などで行うことも可能である。 |
|||
==Javaのジェネリクス== |
|||
Javaでは、2004年のJ2SE 5.0からジェネリクス機能が導入された。それまでのJavaはジェネリックを堅牢性を損ねるものと見送っていたので大きな方針転換となったがその結果、旧バージョンとのバイトコード互換性維持を課せられた後付け実装方式を余儀なくされている。Javaは、ジェネリクスクラスとジェネリクスインターフェースとジェネリッククラスメソッド(=ジェネリック関数)をサポートしている。型パラメータには参照型(オブジェクト)のみ指定可能で、プリミティブ値は不可である。<code>List<Integer></code>は可能だが<code>List<int></code>は不可である。型パラメータはネスト可能である。型パラメータは、上限型境界(<code>extends</code>)と下限型境界(<code>super</code>)で修飾できる。 |
|||
===Adaの例=== |
|||
Javaのジェネリクスの特徴として、型パラメータに指定できるワイルドカード <code><?></code> がある。上限型境界と下限型境界で修飾できるワイルドカードは様々に応用される。ワイルドカードは型パラメータに当てはめられた型の判別を実行時まで遅延させる動的束縛型と同義である。旧バージョンとの互換性から、ランタイム時のジェネリクスインスタンスの型情報は、型パラメータ部分を省いたコンテナ部分だけが保持されている。これはコンパイル時の<code>List<Integer></code>を、ランタイムシステム側はただの<code>List</code>と認識していることを意味する。その型パラメータ判別は、コンテナ要素の実行時型チェックによって代用されている。この仕組みは[[型消去]](''type erasure)と呼ばれている。これはジェネリックプログラミングの静的な解釈に便乗した実装方式であったが、共変反変バリアンスへの拡張を閉ざすことにもなった。ワイルドカードはその補填として用意されたものだったが、共変反変バリアンスとはまた違った性質での柔軟な拡張を可能にしている。ジェネリッククラスの例は以下のようになる。'' |
|||
<syntaxhighlight lang="Java"> |
|||
汎用体パッケージの仕様部 |
|||
List<String> v = new ArrayList<String>(); |
|||
v.add("test"); |
|||
<syntaxhighlight lang="ada"> |
|||
Integer i = (Integer)v.get(0); // (type error) compilation-time error |
|||
generic |
|||
Max_Size : Natural; -- 汎用体仮オブジェクトの例 |
|||
type Element_Type is private; -- 汎用体仮データ型の例; この例では制限型でなければ任意のデータ型が該当 |
|||
package Stacks is |
|||
type Size_Type is range 0 .. Max_Size; |
|||
type Stack is limited private; |
|||
procedure Create (S : out Stack; |
|||
Initial_Size : in Size_Type := Max_Size); |
|||
procedure Push (Into : in out Stack; Element : in Element_Type); |
|||
procedure Pop (From : in out Stack; Element : out Element_Type); |
|||
Overflow : exception; |
|||
Underflow : exception; |
|||
private |
|||
subtype Index_Type is Size_Type range 1 .. Max_Size; |
|||
type Vector is array (Index_Type range <>) of Element_Type; |
|||
type Stack (Allocated_Size : Size_Type := 0) is record |
|||
Top : Index_Type; |
|||
Storage : Vector (1 .. Allocated_Size); |
|||
end record; |
|||
end Stacks; |
|||
</syntaxhighlight> |
</syntaxhighlight> |
||
ジェネリックインターフェースは以下のようになる。 |
|||
<syntaxhighlight lang="Java"> |
|||
public interface List<E> { |
|||
void add(E x); |
|||
Iterator<E> iterator(); |
|||
} |
|||
汎用体パッケージのインスタンス化 |
|||
public interface Iterator<E> { |
|||
E next(); |
|||
<syntaxhighlight lang="ada"> |
|||
boolean hasNext(); |
|||
type Bookmark_Type is new Natural; |
|||
} |
|||
-- 編集中のテキストドキュメント内の場所を記録する |
|||
package Bookmark_Stacks is new Stacks (Max_Size => 20, |
|||
Element_Type => Bookmark_Type); |
|||
-- ドキュメント中の記録された場所にユーザがジャンプできるようにする |
|||
</syntaxhighlight> |
</syntaxhighlight> |
||
汎用体パッケージインスタンスの利用 |
|||
== C#のジェネリクス == |
|||
C#も、Javaに倣った堅牢性方針からジェネリクスを見送っていたが、2005年のver2.0からジェネリクスを採用した。Javaのジェネリクスサポートはソースコードのコンパイル水準に留まり、Java仮想マシンのバイトコード水準の拡張まではなされなかった。それに対してC#は、[[.NET Framework|Microsoft .NET Framework]] 2.0にも拡張を施してランタイムシステム水準からのジェネリクスサポートを整備した。これによってJavaでは出来なかった型パラメータへのプリミティブ値の適用、ジェネリクスが正確に反映されたインスタンスの型情報、ジェネリッククラスの共変性と反変性もサポートできるようになった。パラメータ制約とジェネリクスの入れ子のより自然な解釈も備わっている。C#のジェネリクスクラスは以下のようになる。 |
|||
<syntaxhighlight lang=" |
<syntaxhighlight lang="ada"> |
||
type Document_Type is record |
|||
using System; |
|||
Contents : Ada.Strings.Unbounded.Unbounded_String; |
|||
using System.Collections.Generic; |
|||
Bookmarks : Bookmark_Stacks.Stack; |
|||
end record; |
|||
procedure Edit (Document_Name : in String) is |
|||
static int FirstIndexOfMax<T>(List<T> list) where T: IComparable<T> |
|||
Document : Document_Type; |
|||
{ |
|||
begin |
|||
if (list.Count == 0) { |
|||
-- ブックマークのスタックを初期化 |
|||
return -1; |
|||
Bookmark_Stacks.Create (S => Document.Bookmarks, Initial_Size => 10); |
|||
} |
|||
-- この時点でDocument_Nameファイルを開いたり、読み込んだりが可能 |
|||
int index = -1; |
|||
end Edit; |
|||
for (int i = 0; i < list.Count; ++i) { |
|||
if ((index == -1 && list[i] != null) || |
|||
(index >= 0 && list[index] != null && list[i] != null && list[index].CompareTo(list[i]) < 0)) { |
|||
index = i; |
|||
} |
|||
} |
|||
return index; |
|||
} |
|||
</syntaxhighlight> |
</syntaxhighlight> |
||
===利点と制限=== |
|||
この例では<code>FirstIndexOfMax</code>メソッドの型パラメータ<code>T</code>に対して、<code><nowiki>IComparable<T></nowiki></code>インターフェイスを実装していなければならないという制約を指定している。このことにより、<code><nowiki>IComparable<T></nowiki></code>インターフェイスのメンバである<code>CompareTo</code>メソッドが利用可能になっている。[[C++/CLI]]は.NETのジェネリクスとC++のテンプレート両方をサポートする。ただしこれらの間に互換性はない。 |
|||
Adaの言語構文では、汎用体仮パラメータとして何を許容するか、精密に制約条件を課することができる。例えば実パラメータとしてはモジュラー型(任意の上限で巡回する符号なし整数型)のみを許容するように、仮パラメータとして指定することも可能である。さらには汎用体仮パラメータ間に一定の制約があるように規制することも可能である。例えば、 |
|||
<syntaxhighlight lang="ada"> |
|||
generic |
|||
type Index_Type is (<>); -- 離散型(discrete type)のみを許容 |
|||
type Element_Type is private; -- 制限型(limited type)以外の任意データ型 |
|||
type Array_Type is array (Index_Type range <>) of Element_Type; |
|||
</syntaxhighlight> |
|||
この例でArray_Typeには、Element_Typeに対応する特定のデータ型を要素とし、Index_Typeに対応する特定の離散型の部分型を添字とする配列型でなければならないという制約を課している。プログラマがこの汎用体をインスタンス化する際には、同制約を満足する配列型を実パラメタとして渡さなければならない。 |
|||
構文の複雑さに難はあるものの、精密な制約が表現できることで、汎用体仮パラメータの全ては仕様部として完全に定義される。このため、コンパイラは汎用体本体がなくても汎用体をインスタンス化することができる(もちろん本体がないと[[リンケージエディタ|リンク]]はできない)。 |
|||
C++と異なってAdaでは暗黙的な特化による汎用体のインスタンス化を許さないため、全ての汎用体は明示的にインスタンス化することが必要である。この規則により以下のような結果が生じる。 |
|||
*コンパイラは共有ジェネリクス (shared generics) を実装できる。すなわち、ある汎用体のオブジェクトコードは全インスタンスで共有できる(もちろんプログラマが副プログラムのインライン化を要求しない限り)。さらなる結果として、 |
|||
**コードが肥大化する可能性がない(コードの肥大化はC++では一般的であり後述のように特別な配慮が求められる)。 |
|||
**インスタンス化の都度に新たなオブジェクトコードを生成することは不要であるため、コンパイル時のみならず、実行時に汎用体をインスタンス化することができる。 |
|||
**汎用体仮オブジェクトに対応する実オブジェクトは、たとえ同実オブジェクトが静的である(コンパイル時に値が確定する)としても、汎用体本体中では常に静的ではないものとみなされる。詳細についてはWikibookのGeneric formal objectsを参照。 |
|||
*ある汎用体の全インスタンスは全く同一であるため、他人の作成したプログラムをレビューしたり、理解することが容易である。配慮すべき「特別な場合」はないのだから。 |
|||
*全てのインスタンス化は明示的であり、プログラムの理解が困難となるような暗黙的なインスタンス化はない。 |
|||
*Adaでは特化を許容しないため[[テンプレートメタプログラミング]]はできない。 |
|||
:ただし仮パラメータに精密な制約を課することができるため、例えば、スワップ副プログラムを仮パラメータとして、[[ソート]]を目的とした汎用体の挙動をスワップ対象に応じて変化させたり、離散型の規定演算である大小判定を用いてMaxを実装するなど、特化の利点とされる目的の一部は他の方法により達成することができる。 |
|||
==C++のテンプレート== |
|||
{{main|テンプレート (プログラミング)}} |
|||
C++のテンプレートは関数テンプレート、クラステンプレートをサポートするほか、[[C++14]]では変数テンプレートもサポートするようになった。C++のテンプレートは特に静的な[[ダック・タイピング]]を可能にする点で強力であり、JavaやC#のジェネリクスと比べて柔軟性が高い一方、テンプレート引数に関する制約条件を明示的にコード上で記述できないことからコンパイルエラーメッセージが難解になりやすい。テンプレートはC++言語仕様の複雑化の要因にもなっている。 |
|||
C++の[[Standard Template Library]] (STL) はテンプレートによる汎用的なアルゴリズムとデータ構造を提供する。 |
|||
==D言語のテンプレート== |
==D言語のテンプレート== |
||
D言語はC++のものを発展させたテンプレートをサポートする。大半のC++テンプレートの表現はD言語でもそのまま利用できる。それに加え、D言語は一部の一般的なケースを合理化する機能をいくつか追加する。 |
|||
2006年に初公開された[[D言語]]のテンプレート機能は、強力な[[テンプレートメタプログラミング]]の仕様を備えている。Dの言語仕様自体が、静的コンパイルと通常コンパイルの二段階のコード生成が前提という大きな特徴を備えている。従来のテンプレート機能がプレースホルダ(型パラメータ)に型を当てはめるだけなのに対して、Dのテンプレート機能は値とエイリアスパラメータも当てはめることができる。値とエイリアスパラメータが当てはめられたテンプレート記述は静的コンパイルの対象になり、柔軟なジェネリックコード生成を可能している。静的コンパイルは通常コンパイル前に行われるもので、テンプレート記述ソースをインタプリタ的に解釈していく機能と考えてもよい。値とエイリアスパラメータが用いられるテンプレート記述ソースでは<code>static-if</code>による条件分岐と、静的コンパイル対象関数の再帰による反復処理も可能になっている。エイリアスパラメータは<code>()</code>付記で関数名になり、<code>.</code>付記で左辺がインスタンス名(変数名)右辺がメソッド名になる。エイリアスパラメータをゲッターにする静的コンパイル対象無名変数は[[ヴォルデモート]]型と呼ばれる。静的コンパイル対象関数は<code>template</code>の接頭キーワードで定義される。通常コンパイルのジェネリッククラスとジェネリック関数のプレースホルダ(型パラメータ)宣言は<code>( )</code>を用いる。D言語では一つの<code>( )</code>併記が通常関数で、二つの<code>( )</code>併記がジェネリック関数と見なされる。最初の<code>( )</code>がプレースホルダである。この書式はD言語のテンプレート重視方針の象徴でもある。スペシフィックインスタンス化では<code>!( )</code>を用いる。例としてC++の<code>a<b></code>は、D言語では<code>a!(b)</code>になる。 |
|||
最もはっきりとした違いは一部のシンタックスの変更である。D言語はテンプレートの定義で山形カッコ<code>< ></code>の代わりに丸カッコ<code>( )</code>を使用する。またテンプレートのインスタンス化でも山形カッコの代わりに<code>!( )</code>構文(感嘆符を前に付けた丸カッコ)を使う。従って、D言語の<code>a!(b)</code>はC++の<code>a<b></code>と等価である。この変更は、テンプレート構文の[[構文解析]]を容易にするためになされた(山形カッコは比較演算子との区別がつきにくく、構文解析器が複雑化しがちであった)。 |
|||
===Static-if=== |
|||
静的コンパイル対象関数の例。 |
|||
D言語はコンパイル時に条件をチェックする<code>static if</code>構文を提供する。これはC++の<code>#if</code>と<code>#endif</code>のプリプロセッサマクロに少し似ている。<code>static if</code>はテンプレート引数や、それらを使用したコンパイル時関数実行の結果を含めた全てのコンパイル時の値にアクセスできるというのがその主要な違いである。従ってC++でテンプレートの特殊化を必要とする多くの状況でも、D言語では特殊化の必要なく容易に書ける。D言語の再帰テンプレートは通常の実行時再帰とほぼ同じように書ける。これは典型的なコンパイル時の関数テンプレートに見られる。 |
|||
<syntaxhighlight lang="d"> |
<syntaxhighlight lang="d"> |
||
311行目: | 228行目: | ||
const Factorial = n * Factorial!(n - 1); |
const Factorial = n * Factorial!(n - 1); |
||
} |
} |
||
</syntaxhighlight>エイリアスパラメータ |
</syntaxhighlight> |
||
===エイリアスパラメーター=== |
|||
D言語のテンプレートはまたエイリアスパラメーターを受け入れることができる。エイリアスパラメーターはC++の<code>typedef</code>と似ているが、テンプレートパラメーターを置き換えることもできる。これは今後利用可能なC++0x仕様に追加されるであろう、C++のテンプレートのテンプレート引数にある機能の拡張版である。エイリアスパラメーターは、テンプレート、関数、型、その他のコンパイル時のシンボルを指定できる。これは例えばテンプレート関数の中に関数をプログラマーが''挿入''できるようにする。 |
|||
<syntaxhighlight lang="d"> |
<syntaxhighlight lang="d"> |
||
322行目: | 242行目: | ||
</syntaxhighlight> |
</syntaxhighlight> |
||
この種のテンプレートはC言語APIとD言語のコードを接続するときに使いやすいだろう。仮想のC言語APIが関数ポインタを要求する場合、このようにテンプレートを利用できる。 |
|||
静的コンパイル対象関数は参照化して普通の関数からも使用できる。これは事実上の定数を扱っているのと同義になり実行速度向上に繋がる。ただしその分のコンパイル時間は増える。通常コンパイルで機械コードに落とし込む前に、静的コンパイルで出来ることは全部やっておこうというのがD言語の理念である。 |
|||
<syntaxhighlight lang="d"> |
<syntaxhighlight lang="d"> |
||
331行目: | 251行目: | ||
some_c_function(&wrapper!(foo)); |
some_c_function(&wrapper!(foo)); |
||
</syntaxhighlight> |
</syntaxhighlight> |
||
==Javaのジェネリクス== |
|||
2004年、[[Java Platform, Standard Edition|J2SE]] 5.0の一部として[[Java]]にジェネリクスが追加された。C++のテンプレートとは違い、Javaコードのジェネリクスはジェネリッククラスの1つのコンパイルされたバージョンだけを生成する。ジェネリックJavaクラスは型パラメータとしてオブジェクト型だけを利用できる(基本型は許されない)。従って<code>{{Javadoc:SE|java/util|List}}<{{Javadoc:SE|java/lang|Integer}}></code>は正しいのに対して<code>{{Javadoc:SE|java/util|List}}<int></code>は正しくない。 |
|||
Javaではジェネリクスはコンパイル時に型の正しさをチェックする。そしてジェネリック型情報は[[型消去]] (type erasure) と呼ばれるプロセスを通じて除去され、親クラスの型情報だけが保持される。例えば、<code>{{Javadoc:SE|java/util|List}}<{{Javadoc:SE|java/lang|Integer}}></code>は全てのオブジェクトを保有できる非ジェネリックの(生の){{Javadoc:SE|java/util|List}}に変換されるだろう。しかしながら、コンパイル時のチェックにより、コードが未チェックのコンパイルエラーを生成しない限り、型が正しいようにコードの出力が保証される。 |
|||
このプロセスの典型的な副作用はジェネリック型の情報を実行時に参照できないことである。従って、実行時には、<code>{{Javadoc:SE|java/util|List}}<{{Javadoc:SE|java/lang|Integer}}></code>と<code>{{Javadoc:SE|java/util|List}}<{{Javadoc:SE|java/lang|String}}></code>が同じ{{Javadoc:SE|java/util|List}}クラスであることを示す。この副作用を緩和するひとつの方法は{{Javadoc:SE|java/util|Collection}}の宣言を修飾するJavaの{{Javadoc:SE|name=Collections.checkedList(List<E>, Class<E>)|java/util|Collections|checkedList-java.util.List-java.lang.Class-}}メソッドを利用して、実行時に型付けされた{{Javadoc:SE|java/util|Collection}}の不正利用(例えば不適切な型の挿入)をチェックすることによるものである。これは旧式のコードとジェネリクスを利用するコードを共存運用したい場合の状況で役立つ。 |
|||
C++やC#のように、Javaはネストされたジェネリック型を定義できる。従って、例えば<code>{{Javadoc:SE|java/util|List}}<{{Javadoc:SE|java/util|Map}}<{{Javadoc:SE|java/lang|Integer}}, {{Javadoc:SE|java/lang|String}}>></code>は有効な型である。 |
|||
===ワイルドカード=== |
|||
Javaのジェネリック型パラメーターは特定のクラスに制限されない。与えられたジェネリックオブジェクトが持っているかもしれないパラメーターの型の境界を指定するためにJavaでは'''ワイルドカード'''を使用できる。例えば、<code>{{Javadoc:SE|java/util|List}}<?></code>は無名のオブジェクト型を持つリストを表す。引数として<code><nowiki>List<?></nowiki></code>を取るようなメソッドは任意の型のリストを取ることができる。リストからの読み出しは{{Javadoc:SE|java/lang|Object}}型のオブジェクトを返し、そしてnullではない要素をリストへ書き込むことはパラメーター型が任意ではないために許されない。 |
|||
ジェネリック要素の制約を指定するために、ジェネリック型が境界クラスのサブクラス(クラスの拡張と[[インタフェース (抽象型)|インターフェイス]]の実装のいずれか)であることを示すキーワード<code>extends</code>を使用できる。そして<code>{{Javadoc:SE|java/util|List}}<? extends {{Javadoc:SE|java/lang|Number}}></code>は与えられたリストが{{Javadoc:SE|java/lang|Number}}クラスを拡張するオブジェクトを保持することを意味する。従って、リストが何の要素の型を保持しているのかがわからないためにnullではない要素の書き込みが許されないのに対し、リストから要素を読むと{{Javadoc:SE|java/lang|Number}}が返るだろう。 |
|||
ジェネリック要素の下限を指定するために、ジェネリック型が境界クラスのスーパークラスであることを示すキーワード<code>super</code>が使用される。そして<code>{{Javadoc:SE|java/util|List}}<? super {{Javadoc:SE|java/lang|Number}}></code>は<code>{{Javadoc:SE|java/util|List}}<{{Javadoc:SE|java/lang|Number}}></code>や<code>{{Javadoc:SE|java/util|List}}<{{Javadoc:SE|java/lang|Object}}></code>でありえる。リストに正しい型を保存することが保証されるため任意の{{Javadoc:SE|java/lang|Number}}型の要素をリストに追加できるのに対し、リストからの読み出しでは{{Javadoc:SE|java/lang|Object}}型のオブジェクトを返す。 |
|||
=== 制約 === |
|||
Javaのジェネリクスの実装上の制約により、配列のコンポーネントの型が何であるべきかを特定する方法がないために、ジェネリック型の配列を作成することは不可能である。従って<code><nowiki>new T[size];</nowiki></code>経由のようにメソッドが型引数<code>T</code>を持っていた場合はプログラマはその型の新しい配列を生成することができない。しかし、この制約はJavaの[[リフレクション (情報工学)|リフレクション]]のメカニズムを利用して回避することが可能である。クラス<code>T</code>のインスタンスが利用可能な場合、<code>T</code>に対応する{{Javadoc:SE|java/lang|Class}}オブジェクトのオブジェクトから1つを得て、新しい配列を生成するために{{Javadoc:SE|name=java.lang.reflect.Array.newInstance(Class, int)|java/lang/reflect|Array|newInstance-java.lang.Class-int-}}を使うことができる。もう1つのJavaのジェネリクスの実装上の制約は、<code><?></code>以外に、型パラメーターの型でジェネリッククラスの配列を生成することが不可能であるということだ。これは言語の配列の取り扱い方法に起因するものであり、タイプセーフを維持するために、明示的にキャストしなくともコンパイラが警告を出さないことを全てのコードで保証する必要があるからである。 |
|||
==Haskellのジェネリックプログラミング== |
|||
[[Haskell]]言語にはパラメータ化された型 (parameterized types)、パラメータ多相 (parametric polymorphism)、そしてJavaのジェネリクスやC++のテンプレートの両方に似たプログラミングのスタイルをサポートする型クラス (type classes) がある。Haskellプログラムではこれらの構文を様々なところで利用しており、避けることはかなり難しい。Haskellはまた、さらなるジェネリック性と、多態が提供する以上の再利用性を目指すようにプログラマーと言語開発者を奮起させる、さらに独特なジェネリックプログラミングの機能がある。 |
|||
Haskellの6つの事前定義された型クラス(同一性を比較できる<code>Eq</code>という型と、値を文字列に変換できる<code>Show</code>という型を含む)は''導出インスタンス'' (derived instances) をサポートしている特別なプロパティを持つ。プログラマーが新しい型を定義するということは、クラスのインスタンスを宣言するときに、普通であれば必要なクラスメソッドの実装を提供することなく、この型がこれらの特別型クラスのインスタンスとなることを明示できるということである。全ての必要なメソッドは型の構造に基づいて導出(つまり自動的に生成)される。 |
|||
例として、下記の[[二分木]]型の宣言はこれが<code>Eq</code>と<code>Show</code>のクラスのインスタンスになることを示している。 |
|||
<pre> |
|||
data BinTree a = Leaf a | Node (BinTree a) a (Bintree a) |
|||
deriving (Eq, Show) |
|||
</pre> |
|||
<code>T</code>がそれらの演算子を自分でサポートしているのであれば、任意の型の<code>BinTree T</code>形式のために比較関数 (<code>==</code>) と文字列表現関数 (<code>show</code>) が自動的に定義される。 |
|||
<code>Eq</code>と<code>Show</code>の導出インスタンスへのサポートは、それらのメソッドである<code>==</code>と<code>show</code>を、パラメーター的な多態関数とは質的に異なるジェネリックにする。これらの"関数"(より正確には型でインデックス付けられた (type-indexed) 関数のファミリー)はたくさんの異なる型の値を受け入れることができ、各引数の型によってそれらは異なる動作をするが、新しい型へのサポートを追加するためにわずかな作業が必要とされる。Ralf Hinze氏 (2004) は、あるプログラミングテクニックによりユーザー定義型のクラスに対して同様の結果を達成できることを示した。彼以外の多くの研究者はこれと、Haskellの流れとは違う種類のジェネリック性やHaskellの拡張(下記参照)に対する取り組みを提案していた。 |
|||
=== PolyP === |
|||
PolyPはHaskellに対する最初のジェネリックプログラミング言語拡張であった。PolyPではジェネリック関数は''polytypic''と呼ばれた。通常データ型のパターン[[ファンクタ]]の構造によって構造的な導出を通じて定義できるpolytypic関数のような特別な構文を言語に導入した。PolyPでの通常データ型はHaskellのデータ型のサブセットである。通常データ型tは''* → *''の種類でなければならず、もし''a''が定義における表面的な型の引数である場合は、''t''に対する全ての再帰呼び出しは''t a''形式でなければならない。これらの制約は、異なる形式の再帰呼び出しである入れ子のデータタイプと同様に、上位に種類付けされたデータ型を規定する。 |
|||
PolyPの展開された関数はここに例として示される。 |
|||
<pre> |
|||
flatten :: Regular d => d a -> [a] |
|||
flatten = cata fl |
|||
polytypic fl :: f a [a] -> [a] |
|||
case f of |
|||
g+h -> either fl fl |
|||
g*h -> \(x,y) -> fl x ++ fl y |
|||
() -> \x -> [] |
|||
Par -> \x -> [x] |
|||
Rec -> \x -> x |
|||
d@g -> concat . flatten . pmap fl |
|||
Con t -> \x -> [] |
|||
cata :: Regular d => (FunctorOf d a b -> b) -> d a -> b |
|||
</pre> |
|||
===ジェネリックHaskell=== |
|||
ジェネリックHaskellは[[ユトレヒト大学]]で開発されたHaskellのもう1つの拡張だ。この拡張は下記の特徴がある。 |
|||
*''Type-indexed values''は様々なHaskell型のコンストラクタ(ユニット、基本型、合計、積、ユーザー定義型のコンストラクタ)に渡ってインデックス付けられた値として定義される。さらに''コンストラクタケース''を使って特定のコンストラクタに対してtype-indexed valuesの動作を指定することもでき、''デフォルトケース''を使ったもう一つの中で1つのジェネリック定義を再利用することもできる。 |
|||
type-indexed valueの結果は任意の型に特殊化され得る。 |
|||
*''Kind-indexed types''は''*''と''k → k''の両方のケースを与えることで定義された種別に対してインデックス付けられた型である。インスタンスは種別にkind-indexed typeを適用することで得られる。 |
|||
*ジェネリック定義は型もしくは種別にそれらを適用することで利用できる。これは''ジェネリックアプリケーション''と呼ばれる。どの種類のジェネリック定義が適用されたかに依存して結果は型か値になる。 |
|||
*''Generic abstraction''はジェネリック定義が(与えられた種別の)型パラメーターの抽象化で定義されることを可能にする。 |
|||
*''Type-indexed types''は型コンストラクタに対してインデックス付けられた型である。これらは型がもっとジェネリック値に取り入るために利用できる。type-indexed typesの結果は任意の型に特殊化され得る。 |
|||
ジェネリックHaskellの比較関数の一例として。 |
|||
<pre> |
|||
type Eq {[ * ]} t1 t2 = t1 -> t2 -> Bool |
|||
type Eq {[ k -> l ]} t1 t2 = forall u1 u2. Eq {[ k ]} u1 u2 -> Eq {[ l ]} (t1 u1) (t2 u2) |
|||
eq {| t :: k |} :: Eq {[ k ]} t t |
|||
eq {| Unit |} _ _ = True |
|||
eq {| :+: |} eqA eqB (Inl a1) (Inl a2) = eqA a1 a2 |
|||
eq {| :+: |} eqA eqB (Inr b1) (Inr b2) = eqB b1 b2 |
|||
eq {| :+: |} eqA eqB _ _ = False |
|||
eq {| :*: |} eqA eqB (a1 :*: b1) (a2 :*: b2) = eqA a1 a2 && eqB b1 b2 |
|||
eq {| Int |} = (==) |
|||
eq {| Char |} = (==) |
|||
eq {| Bool |} = (==) |
|||
</pre> |
|||
===「決まり文句を捨てる」アプローチ=== |
|||
決まり文句を捨てるアプローチ (Scrap your boilerplate approach) は簡易的なジェネリックプログラミングのHaskellに対するアプローチである (Lämmel and Peyton Jones, 2003)。このアプローチはHaskellのGHC>=6.0の実装でサポートされる。このアプローチを使うことで、ジェネリックな読み込み、ジェネリックな明示、ジェネリックな比較(つまりgread、gshow、geq)と同様に、横断スキーム(例えばいつでもどこでも)のようなジェネリック関数をプログラマーは記述できる。このアプローチはタイプセーフなキャストとコンストラクタアプリケーションの実行のための一部の基本要素に基づいている。 |
|||
==C#と.NETのジェネリックプログラミング== |
|||
C#(およびその他の.NET言語)のジェネリクスは.NET Framework 2.0の一部として2005年11月に追加された。Javaと似てはいるが、.NETのジェネリクスは、コンパイラによるジェネリクス型から非ジェネリクス型へのコンバートとしてではなく、実行時に実装される。このことにより、ジェネリクス型に関するあらゆる情報はメタデータとして保存される。 |
|||
.NETジェネリクスの機能 |
|||
*型情報を削除せず、[[共通言語ランタイム|CLR]]の内部でジェネリクスが構築されるため(そしてコンパイラ上では全く構築しないため)、キャストや動的チェックの実行からくるパフォーマンスヒットがない。また、プログラマーはリフレクションを通じてジェネリック情報にアクセスできる。 |
|||
**型情報を削除しないので、Javaでは不可能なジェネリック型の配列の生成が可能。 |
|||
*ジェネリック型の引数として参照型だけでなく値型(組み込みの基本型、およびユーザー定義型の両方)も利用できる。値型の場合、JITコンパイラは特殊化のためにネイティブコードの新しいインスタンスを作成する。このことにより[[ボックス化]]をする必要がなくなり、パフォーマンスが向上する。 |
|||
*Javaと同様、ジェネリック型引数がそれら自身のジェネリック型であるようにできる。つまり、<code><nowiki>List<List<Dictionary<int, int>>></nowiki></code>のような型は有効である。 |
|||
*C#(および一般の.NET)は、キーワード<code>where</code>を使用することで、値型/参照型、デフォルトコンストラクタの存在、親クラス、実装するインターフェイスなどでジェネリック型を制約することができる。 |
|||
*[[共変性と反変性 (計算機科学)|共変性と反変性]]をサポートしている。C# 4.0以降ではout修飾子またはin修飾子により、型パラメータを共変または反変にすることができる。これによって、ジェネリック型の代入と使用の柔軟性が向上する。 |
|||
<syntaxhighlight lang="csharp"> |
|||
using System; |
|||
using System.Collections.Generic; |
|||
static int FirstIndexOfMax<T>(List<T> list) where T: IComparable<T> |
|||
{ |
|||
if (list.Count == 0) { |
|||
return -1; |
|||
} |
|||
int index = -1; |
|||
for (int i = 0; i < list.Count; ++i) { |
|||
if ((index == -1 && list[i] != null) || |
|||
(index >= 0 && list[index] != null && list[i] != null && list[index].CompareTo(list[i]) < 0)) { |
|||
index = i; |
|||
} |
|||
} |
|||
return index; |
|||
} |
|||
</syntaxhighlight> |
|||
この例では<code>FirstIndexOfMax</code>メソッドの型パラメータ<code>T</code>に対して、<code><nowiki>IComparable<T></nowiki></code>インターフェイスを実装していなければならないという制約を指定している。このことにより、<code><nowiki>IComparable<T></nowiki></code>インターフェイスのメンバである<code>CompareTo</code>メソッドが利用可能になっている。 |
|||
[[C++/CLI]]は.NETのジェネリクスとC++のテンプレート両方をサポートする。ただしこれらの間に互換性はない。 |
|||
==その他の言語のジェネリックプログラミング機能== |
|||
数多くの関数型言語はパラメータ化された型 (parameterized types) とパラメータ多相 (parametric polymorphism) の形で小規模なジェネリックプログラミングをサポートする。さらに標準MLとOCamlはクラステンプレートとAdaのジェネリックパッケージに似たファンクタを提供する。 |
|||
[[Verilog]]のモジュールは1つ以上のパラメタを取ることができる。パラメタの実際の値は、そのモジュールを実体化する際に与えられる。一例としてジェネリックな[[:en:Hardware register|レジスタ]]アレイがあり、アレイの幅がパラメタで与えられている。そのようなアレイをジェネリックなワイヤベクトルと組み合わせることにより、単一のモジュール実装を用いて任意のビット幅を持つジェネリックなバッファやメモリを作ることができる。<ref>Verilog by Example, Section ''The Rest for Reference''. Blaine C. Readler, Full Arc Press, 2011. ISBN 978-0-9834973-0-1</ref> |
|||
== 脚注 == |
== 脚注 == |
||
336行目: | 384行目: | ||
== 関連項目 == |
== 関連項目 == |
||
* [[総称型]] |
|||
*[[テンプレートメタプログラミング]] |
|||
*[[ポリモーフィズム]] |
* [[ポリモーフィズム]] |
||
* [[ダック・タイピング]] |
|||
{{プログラミング言語の関連項目}} |
|||
{{DEFAULTSORT:しえねりつくふろくらみんく}} |
{{DEFAULTSORT:しえねりつくふろくらみんく}} |
||
[[Category:ソフトウェア工学]] |
[[Category:ソフトウェア工学]] |
2021年2月16日 (火) 02:19時点における版
ジェネリック(総称あるいは汎用)プログラミング(英: generic programming)は、具体的なデータ型に直接依存しない、抽象的かつ汎用的なコード記述を可能にするコンピュータプログラミング手法である。
概要
ジェネリックプログラミングはデータ型でコードをインスタンス化するのか、あるいはデータ型をパラメータとして渡すかということにかかわらず、同じソースコードを利用できる[1]。ジェネリックプログラミングは言語により異なる形で実装されている。ジェネリックプログラミングの機能は1970年代にCLUやAdaのような言語に搭載され、次にBETA、C++、D、Eiffel、Java、その後DECのTrellis/Owl言語などの数多くのオブジェクトベース (object-based) およびオブジェクト指向 (object-oriented) 言語に採用された。
1995年の書籍デザインパターン[要文献特定詳細情報]の共著者[誰?]は(Ada、Eiffel、Java、C#の)ジェネリクスや、(C++の)テンプレートとしても知られるパラメータ化された型 (parameterized types) としてジェネリクスについて触れている。これらは、型を指定することなく、型を定義できるようにする(型は使用する時点で引数として与えられる)。このテクニック(特にデリゲーションを組み合わせるとき)は非常に強力である。
特徴
ジェネリックプログラミングの特徴は、型を抽象化してコードの再利用性を向上させつつ、静的型付け言語の持つ型安全性を維持できることである。
ジェネリックプログラミングを用いない場合、例えば伝統的なC言語やPascalのような従来の静的型付け言語において、ソートなどのアルゴリズムや連結リストのようなデータ構造(オブジェクトのコンテナ)を記述する際は、たとえ対象となる要素のデータ型が異なるだけで事実上同一のコードであったとしても、具体的なデータ型ごとにそれぞれ実装しなければならない。整数型のリスト、倍精度浮動小数点数型のリスト、文字列型のリスト、ユーザー定義構造体のリスト、……といった具合である。もしジェネリックプログラミングをサポートしない言語で汎用的なコードを記述して再利用しようと思えば、メモリ空間効率や型安全性などを犠牲にしなければならなくなる(共用体や汎用ポインタ型とキャストを駆使するなど)。一方、C++の関数テンプレートやクラステンプレートのように、ジェネリックプログラミングを用いることで、抽象化された型について一度だけ記述したアルゴリズムやデータ構造をさまざまな具象データ型に適用して、コードを型安全に再利用できるようになる。これがジェネリックプログラミングの利点の一例として挙げられる。
以下にC++の例を示す。
template<typename T>
class LinkedList {
public:
// 双方向連結リストのノード。
class Node {
friend class LinkedList;
public:
T value;
private:
Node* prev;
Node* next;
private:
Node() : value(), prev(), next() {}
explicit Node(const T& value, Node* prev = NULL, Node* next = NULL) : value(value), prev(prev), next(next) {}
~Node() {}
public:
Node* getPrev() { return this->prev; }
Node* getNext() { return this->next; }
};
private:
Node dummy;
public:
LinkedList() : dummy() {
this->dummy.prev = &this->dummy;
this->dummy.next = &this->dummy;
}
~LinkedList() { this->clear(); }
size_t getSize() const { /* ... */ }
Node* getHead() { return this->dummy.next; }
Node* getTail() { return this->dummy.prev; }
Node* getSentinel() { return &this->dummy; }
static Node* insertBefore(Node* node, const T& value) {
assert(node);
assert(node->prev);
Node* temp = new Node(value, node->prev, node);
node->prev->next = temp;
node->prev = temp;
return temp;
}
static Node* insertAfter(Node* node, const T& value) {
assert(node);
assert(node->next);
Node* temp = new Node(value, node, node->next);
node->next->prev = temp;
node->next = temp;
return temp;
}
static void remove(Node*& node) {
assert(node);
if (node->prev) { node->prev->next = node->next; }
if (node->next) { node->next->prev = node->prev; }
delete node;
node = NULL;
}
void clear() {
for (Node* current = this->getHead(); current != this->getSentinel(); ) {
Node* temp = current;
current = current->next;
delete temp;
}
this->dummy.prev = &this->dummy;
this->dummy.next = &this->dummy;
}
};
LinkedList<int> list_of_integers;
LinkedList<Animal> list_of_animals;
LinkedList<Car> list_of_cars;
上記は要素型をT
とする双方向連結リストの定義例である。typename T
はテンプレートによる抽象化の対象となる型の名前(プレースホルダー)を表す。そしてこの定義されたクラステンプレートのインスタンス化、すなわち型パラメータT
に具象型を与えることによって生成されるクラス型は、T
について実際に指定した具象型のリストとして扱われる。これらの「T型のコンテナ」を一般にジェネリクス (generics) と呼び、ジェネリックプログラミングの代表的なテクニックである。プログラミング言語によって制約は様々だが、このテクニックは、継承関係やシグネチャといった制約条件 (constraint) を維持する限り、内包するT
にあらゆるデータ型を指定可能なクラスの定義を可能にする。これはジェネリックプログラミングの典型であり、一部の言語[要説明]ではこの形式のみを実装する。ただし、概念としてのジェネリックプログラミングはジェネリクスに限定されない。
オブジェクト指向プログラミング言語は、サブタイプ(派生型)でスーパータイプ(基底型)の振る舞い(アルゴリズム)をオーバーライドすることによる動的なポリモーフィズム(多態性)を備えており、動的な多態性もまたスーパータイプによる抽象化とサブタイプによる具象化[2]を実現するものだが、ジェネリクスは静的な多態性による抽象化と具象化を実現するという点で設計を異にする。
ジェネリックプログラミングのもう一つの応用例として、型に依存しないスワップ関数の例を示す。
template<typename T>
void Swap(T& a, T& b) // "&"により参照としてパラメーターを渡している。
{
T temp = b;
b = a;
a = temp;
}
using namespace std;
string s1 = "world!", s2 = "Hello, ";
Swap(s1, s2);
cout << s1 << s2 << endl; // 出力は"Hello, world!"
上記の例で使用したC++のtemplate
文は、プログラマーや言語の開発者たちにこの概念を普及させたジェネリックプログラミングの例といわれている。この構文はジェネリックプログラミングの全ての概念に対応する。またD言語はC++のテンプレートを基に構文を単純化した完全なジェネリックの機能を提供する。JavaはJ2SE 5.0よりC++の文法に近いジェネリックプログラミングの機能を提供しており、ジェネリクス(「T型のコンテナ」)という、ジェネリックプログラミングの部分集合を実装する。
C# 2.0、Visual Basic .NET 2005 (VB 8.0) では、Microsoft .NET Framework 2.0がサポートするジェネリクスを利用するための構文が追加された。MLファミリーはパラメータ多相 (parametric polymorphism) とファンクタと呼ばれるジェネリックモジュールを利用してのジェネリックプログラミングを推奨する。Haskellのタイプクラスのメカニズムもまたジェネリックプログラミングに対応する。
Objective-Cにあるような動的型付けを使い、必要に応じて注意深くコーディング規約を守れば[要説明]、ジェネリックプログラミングの技術を使う必要がなくなる。全てのオブジェクトを包括する汎用型があるためである。Javaもまたそうであるが、キャストが必要なので静的な型付けの統一性を乱してしまう。例えば、ジェネリクスをサポートしていなかった時代のJavaでは、List
のようなコレクションに格納できる要素型はObject
のみであったため、要素取り出しの際には実際のサブクラス型への適切なキャストが必要だった。それに対し、ジェネリクスは静的な型付けについての利点を持ちながら動的な型付けの利点を完全ではないが得られる方法である。
Adaのジェネリクス
Adaには1977年-1980年の設計当初から汎用体 (generics) が存在する。標準ライブラリでも多くのサービスを実装するために汎用体を用いている。Ada2005では1998年に規格化されたC++のStandard Template Library (STL) の影響を受けた広範な汎用コンテナが標準ライブラリとして追加された。
汎用体 (generic unit) とは、0または複数の汎用体仮パラメータ (generic formal parameters) を採るプログラム単位(パッケージまたは副プログラム)である。
汎用体仮パラメータとしては、オブジェクト(変数・定数)、データ型、副プログラム、パッケージ,さらには他の汎用体のインスタンスさえ指定することができる。汎用体仮パラメータのデータ型としては、離散 (discrete) 型、浮動小数点数型、固定小数点数型、アクセス(ポインタ)型などを用いることができる。
汎用体をインスタンス化する際、プログラマは全ての仮パラメータに対応する実パラメータを指定する必要があるが、プログラマが明示的に全ての実パラメータを指定しなくても済むよう,仮パラメータにはデフォルトを指定することもできる。インスタンス化してしまえば,汎用体のインスタンスは、汎用体ではない通常のプログラム単位であるかのように振舞う。インスタンス化は実行時、例えばループの中などで行うことも可能である。
Adaの例
汎用体パッケージの仕様部
generic
Max_Size : Natural; -- 汎用体仮オブジェクトの例
type Element_Type is private; -- 汎用体仮データ型の例; この例では制限型でなければ任意のデータ型が該当
package Stacks is
type Size_Type is range 0 .. Max_Size;
type Stack is limited private;
procedure Create (S : out Stack;
Initial_Size : in Size_Type := Max_Size);
procedure Push (Into : in out Stack; Element : in Element_Type);
procedure Pop (From : in out Stack; Element : out Element_Type);
Overflow : exception;
Underflow : exception;
private
subtype Index_Type is Size_Type range 1 .. Max_Size;
type Vector is array (Index_Type range <>) of Element_Type;
type Stack (Allocated_Size : Size_Type := 0) is record
Top : Index_Type;
Storage : Vector (1 .. Allocated_Size);
end record;
end Stacks;
汎用体パッケージのインスタンス化
type Bookmark_Type is new Natural;
-- 編集中のテキストドキュメント内の場所を記録する
package Bookmark_Stacks is new Stacks (Max_Size => 20,
Element_Type => Bookmark_Type);
-- ドキュメント中の記録された場所にユーザがジャンプできるようにする
汎用体パッケージインスタンスの利用
type Document_Type is record
Contents : Ada.Strings.Unbounded.Unbounded_String;
Bookmarks : Bookmark_Stacks.Stack;
end record;
procedure Edit (Document_Name : in String) is
Document : Document_Type;
begin
-- ブックマークのスタックを初期化
Bookmark_Stacks.Create (S => Document.Bookmarks, Initial_Size => 10);
-- この時点でDocument_Nameファイルを開いたり、読み込んだりが可能
end Edit;
利点と制限
Adaの言語構文では、汎用体仮パラメータとして何を許容するか、精密に制約条件を課することができる。例えば実パラメータとしてはモジュラー型(任意の上限で巡回する符号なし整数型)のみを許容するように、仮パラメータとして指定することも可能である。さらには汎用体仮パラメータ間に一定の制約があるように規制することも可能である。例えば、
generic
type Index_Type is (<>); -- 離散型(discrete type)のみを許容
type Element_Type is private; -- 制限型(limited type)以外の任意データ型
type Array_Type is array (Index_Type range <>) of Element_Type;
この例でArray_Typeには、Element_Typeに対応する特定のデータ型を要素とし、Index_Typeに対応する特定の離散型の部分型を添字とする配列型でなければならないという制約を課している。プログラマがこの汎用体をインスタンス化する際には、同制約を満足する配列型を実パラメタとして渡さなければならない。
構文の複雑さに難はあるものの、精密な制約が表現できることで、汎用体仮パラメータの全ては仕様部として完全に定義される。このため、コンパイラは汎用体本体がなくても汎用体をインスタンス化することができる(もちろん本体がないとリンクはできない)。
C++と異なってAdaでは暗黙的な特化による汎用体のインスタンス化を許さないため、全ての汎用体は明示的にインスタンス化することが必要である。この規則により以下のような結果が生じる。
- コンパイラは共有ジェネリクス (shared generics) を実装できる。すなわち、ある汎用体のオブジェクトコードは全インスタンスで共有できる(もちろんプログラマが副プログラムのインライン化を要求しない限り)。さらなる結果として、
- コードが肥大化する可能性がない(コードの肥大化はC++では一般的であり後述のように特別な配慮が求められる)。
- インスタンス化の都度に新たなオブジェクトコードを生成することは不要であるため、コンパイル時のみならず、実行時に汎用体をインスタンス化することができる。
- 汎用体仮オブジェクトに対応する実オブジェクトは、たとえ同実オブジェクトが静的である(コンパイル時に値が確定する)としても、汎用体本体中では常に静的ではないものとみなされる。詳細についてはWikibookのGeneric formal objectsを参照。
- ある汎用体の全インスタンスは全く同一であるため、他人の作成したプログラムをレビューしたり、理解することが容易である。配慮すべき「特別な場合」はないのだから。
- 全てのインスタンス化は明示的であり、プログラムの理解が困難となるような暗黙的なインスタンス化はない。
- Adaでは特化を許容しないためテンプレートメタプログラミングはできない。
- ただし仮パラメータに精密な制約を課することができるため、例えば、スワップ副プログラムを仮パラメータとして、ソートを目的とした汎用体の挙動をスワップ対象に応じて変化させたり、離散型の規定演算である大小判定を用いてMaxを実装するなど、特化の利点とされる目的の一部は他の方法により達成することができる。
C++のテンプレート
C++のテンプレートは関数テンプレート、クラステンプレートをサポートするほか、C++14では変数テンプレートもサポートするようになった。C++のテンプレートは特に静的なダック・タイピングを可能にする点で強力であり、JavaやC#のジェネリクスと比べて柔軟性が高い一方、テンプレート引数に関する制約条件を明示的にコード上で記述できないことからコンパイルエラーメッセージが難解になりやすい。テンプレートはC++言語仕様の複雑化の要因にもなっている。
C++のStandard Template Library (STL) はテンプレートによる汎用的なアルゴリズムとデータ構造を提供する。
D言語のテンプレート
D言語はC++のものを発展させたテンプレートをサポートする。大半のC++テンプレートの表現はD言語でもそのまま利用できる。それに加え、D言語は一部の一般的なケースを合理化する機能をいくつか追加する。
最もはっきりとした違いは一部のシンタックスの変更である。D言語はテンプレートの定義で山形カッコ< >
の代わりに丸カッコ( )
を使用する。またテンプレートのインスタンス化でも山形カッコの代わりに!( )
構文(感嘆符を前に付けた丸カッコ)を使う。従って、D言語のa!(b)
はC++のa<b>
と等価である。この変更は、テンプレート構文の構文解析を容易にするためになされた(山形カッコは比較演算子との区別がつきにくく、構文解析器が複雑化しがちであった)。
Static-if
D言語はコンパイル時に条件をチェックするstatic if
構文を提供する。これはC++の#if
と#endif
のプリプロセッサマクロに少し似ている。static if
はテンプレート引数や、それらを使用したコンパイル時関数実行の結果を含めた全てのコンパイル時の値にアクセスできるというのがその主要な違いである。従ってC++でテンプレートの特殊化を必要とする多くの状況でも、D言語では特殊化の必要なく容易に書ける。D言語の再帰テンプレートは通常の実行時再帰とほぼ同じように書ける。これは典型的なコンパイル時の関数テンプレートに見られる。
template Factorial(ulong n) {
static if (n <= 1)
const Factorial = 1u;
else
const Factorial = n * Factorial!(n - 1);
}
エイリアスパラメーター
D言語のテンプレートはまたエイリアスパラメーターを受け入れることができる。エイリアスパラメーターはC++のtypedef
と似ているが、テンプレートパラメーターを置き換えることもできる。これは今後利用可能なC++0x仕様に追加されるであろう、C++のテンプレートのテンプレート引数にある機能の拡張版である。エイリアスパラメーターは、テンプレート、関数、型、その他のコンパイル時のシンボルを指定できる。これは例えばテンプレート関数の中に関数をプログラマーが挿入できるようにする。
template wrapper(alias Fn) {
// "extern(C)"インターフェイスでD言語の関数をラップする
extern(C) void wrapper() {
Fn();
}
}
この種のテンプレートはC言語APIとD言語のコードを接続するときに使いやすいだろう。仮想のC言語APIが関数ポインタを要求する場合、このようにテンプレートを利用できる。
void foo() {
// ...
}
some_c_function(&wrapper!(foo));
Javaのジェネリクス
2004年、J2SE 5.0の一部としてJavaにジェネリクスが追加された。C++のテンプレートとは違い、Javaコードのジェネリクスはジェネリッククラスの1つのコンパイルされたバージョンだけを生成する。ジェネリックJavaクラスは型パラメータとしてオブジェクト型だけを利用できる(基本型は許されない)。従って
は正しいのに対してList
<Integer
>
は正しくない。
List
<int>
Javaではジェネリクスはコンパイル時に型の正しさをチェックする。そしてジェネリック型情報は型消去 (type erasure) と呼ばれるプロセスを通じて除去され、親クラスの型情報だけが保持される。例えば、
は全てのオブジェクトを保有できる非ジェネリックの(生の)List
<Integer
>List
に変換されるだろう。しかしながら、コンパイル時のチェックにより、コードが未チェックのコンパイルエラーを生成しない限り、型が正しいようにコードの出力が保証される。
このプロセスの典型的な副作用はジェネリック型の情報を実行時に参照できないことである。従って、実行時には、
とList
<Integer
>
が同じList
<String
>List
クラスであることを示す。この副作用を緩和するひとつの方法はCollection
の宣言を修飾するJavaのCollections.checkedList(List<E>, Class<E>)
メソッドを利用して、実行時に型付けされたCollection
の不正利用(例えば不適切な型の挿入)をチェックすることによるものである。これは旧式のコードとジェネリクスを利用するコードを共存運用したい場合の状況で役立つ。
C++やC#のように、Javaはネストされたジェネリック型を定義できる。従って、例えば
は有効な型である。
List
<Map
<Integer
, String
>>
ワイルドカード
Javaのジェネリック型パラメーターは特定のクラスに制限されない。与えられたジェネリックオブジェクトが持っているかもしれないパラメーターの型の境界を指定するためにJavaではワイルドカードを使用できる。例えば、
は無名のオブジェクト型を持つリストを表す。引数としてList
<?>List<?>
を取るようなメソッドは任意の型のリストを取ることができる。リストからの読み出しはObject
型のオブジェクトを返し、そしてnullではない要素をリストへ書き込むことはパラメーター型が任意ではないために許されない。
ジェネリック要素の制約を指定するために、ジェネリック型が境界クラスのサブクラス(クラスの拡張とインターフェイスの実装のいずれか)であることを示すキーワードextends
を使用できる。そして
は与えられたリストがList
<? extends Number
>Number
クラスを拡張するオブジェクトを保持することを意味する。従って、リストが何の要素の型を保持しているのかがわからないためにnullではない要素の書き込みが許されないのに対し、リストから要素を読むとNumber
が返るだろう。
ジェネリック要素の下限を指定するために、ジェネリック型が境界クラスのスーパークラスであることを示すキーワードsuper
が使用される。そして
はList
<? super Number
>
やList
<Number
>
でありえる。リストに正しい型を保存することが保証されるため任意のList
<Object
>Number
型の要素をリストに追加できるのに対し、リストからの読み出しではObject
型のオブジェクトを返す。
制約
Javaのジェネリクスの実装上の制約により、配列のコンポーネントの型が何であるべきかを特定する方法がないために、ジェネリック型の配列を作成することは不可能である。従ってnew T[size];
経由のようにメソッドが型引数T
を持っていた場合はプログラマはその型の新しい配列を生成することができない。しかし、この制約はJavaのリフレクションのメカニズムを利用して回避することが可能である。クラスT
のインスタンスが利用可能な場合、T
に対応するClass
オブジェクトのオブジェクトから1つを得て、新しい配列を生成するためにjava.lang.reflect.Array.newInstance(Class, int)
を使うことができる。もう1つのJavaのジェネリクスの実装上の制約は、<?>
以外に、型パラメーターの型でジェネリッククラスの配列を生成することが不可能であるということだ。これは言語の配列の取り扱い方法に起因するものであり、タイプセーフを維持するために、明示的にキャストしなくともコンパイラが警告を出さないことを全てのコードで保証する必要があるからである。
Haskellのジェネリックプログラミング
Haskell言語にはパラメータ化された型 (parameterized types)、パラメータ多相 (parametric polymorphism)、そしてJavaのジェネリクスやC++のテンプレートの両方に似たプログラミングのスタイルをサポートする型クラス (type classes) がある。Haskellプログラムではこれらの構文を様々なところで利用しており、避けることはかなり難しい。Haskellはまた、さらなるジェネリック性と、多態が提供する以上の再利用性を目指すようにプログラマーと言語開発者を奮起させる、さらに独特なジェネリックプログラミングの機能がある。
Haskellの6つの事前定義された型クラス(同一性を比較できるEq
という型と、値を文字列に変換できるShow
という型を含む)は導出インスタンス (derived instances) をサポートしている特別なプロパティを持つ。プログラマーが新しい型を定義するということは、クラスのインスタンスを宣言するときに、普通であれば必要なクラスメソッドの実装を提供することなく、この型がこれらの特別型クラスのインスタンスとなることを明示できるということである。全ての必要なメソッドは型の構造に基づいて導出(つまり自動的に生成)される。
例として、下記の二分木型の宣言はこれがEq
とShow
のクラスのインスタンスになることを示している。
data BinTree a = Leaf a | Node (BinTree a) a (Bintree a) deriving (Eq, Show)
T
がそれらの演算子を自分でサポートしているのであれば、任意の型のBinTree T
形式のために比較関数 (==
) と文字列表現関数 (show
) が自動的に定義される。
Eq
とShow
の導出インスタンスへのサポートは、それらのメソッドである==
とshow
を、パラメーター的な多態関数とは質的に異なるジェネリックにする。これらの"関数"(より正確には型でインデックス付けられた (type-indexed) 関数のファミリー)はたくさんの異なる型の値を受け入れることができ、各引数の型によってそれらは異なる動作をするが、新しい型へのサポートを追加するためにわずかな作業が必要とされる。Ralf Hinze氏 (2004) は、あるプログラミングテクニックによりユーザー定義型のクラスに対して同様の結果を達成できることを示した。彼以外の多くの研究者はこれと、Haskellの流れとは違う種類のジェネリック性やHaskellの拡張(下記参照)に対する取り組みを提案していた。
PolyP
PolyPはHaskellに対する最初のジェネリックプログラミング言語拡張であった。PolyPではジェネリック関数はpolytypicと呼ばれた。通常データ型のパターンファンクタの構造によって構造的な導出を通じて定義できるpolytypic関数のような特別な構文を言語に導入した。PolyPでの通常データ型はHaskellのデータ型のサブセットである。通常データ型tは* → *の種類でなければならず、もしaが定義における表面的な型の引数である場合は、tに対する全ての再帰呼び出しはt a形式でなければならない。これらの制約は、異なる形式の再帰呼び出しである入れ子のデータタイプと同様に、上位に種類付けされたデータ型を規定する。
PolyPの展開された関数はここに例として示される。
flatten :: Regular d => d a -> [a] flatten = cata fl polytypic fl :: f a [a] -> [a] case f of g+h -> either fl fl g*h -> \(x,y) -> fl x ++ fl y () -> \x -> [] Par -> \x -> [x] Rec -> \x -> x d@g -> concat . flatten . pmap fl Con t -> \x -> [] cata :: Regular d => (FunctorOf d a b -> b) -> d a -> b
ジェネリックHaskell
ジェネリックHaskellはユトレヒト大学で開発されたHaskellのもう1つの拡張だ。この拡張は下記の特徴がある。
- Type-indexed valuesは様々なHaskell型のコンストラクタ(ユニット、基本型、合計、積、ユーザー定義型のコンストラクタ)に渡ってインデックス付けられた値として定義される。さらにコンストラクタケースを使って特定のコンストラクタに対してtype-indexed valuesの動作を指定することもでき、デフォルトケースを使ったもう一つの中で1つのジェネリック定義を再利用することもできる。
type-indexed valueの結果は任意の型に特殊化され得る。
- Kind-indexed typesは*とk → kの両方のケースを与えることで定義された種別に対してインデックス付けられた型である。インスタンスは種別にkind-indexed typeを適用することで得られる。
- ジェネリック定義は型もしくは種別にそれらを適用することで利用できる。これはジェネリックアプリケーションと呼ばれる。どの種類のジェネリック定義が適用されたかに依存して結果は型か値になる。
- Generic abstractionはジェネリック定義が(与えられた種別の)型パラメーターの抽象化で定義されることを可能にする。
- Type-indexed typesは型コンストラクタに対してインデックス付けられた型である。これらは型がもっとジェネリック値に取り入るために利用できる。type-indexed typesの結果は任意の型に特殊化され得る。
ジェネリックHaskellの比較関数の一例として。
type Eq {[ * ]} t1 t2 = t1 -> t2 -> Bool type Eq {[ k -> l ]} t1 t2 = forall u1 u2. Eq {[ k ]} u1 u2 -> Eq {[ l ]} (t1 u1) (t2 u2) eq {| t :: k |} :: Eq {[ k ]} t t eq {| Unit |} _ _ = True eq {| :+: |} eqA eqB (Inl a1) (Inl a2) = eqA a1 a2 eq {| :+: |} eqA eqB (Inr b1) (Inr b2) = eqB b1 b2 eq {| :+: |} eqA eqB _ _ = False eq {| :*: |} eqA eqB (a1 :*: b1) (a2 :*: b2) = eqA a1 a2 && eqB b1 b2 eq {| Int |} = (==) eq {| Char |} = (==) eq {| Bool |} = (==)
「決まり文句を捨てる」アプローチ
決まり文句を捨てるアプローチ (Scrap your boilerplate approach) は簡易的なジェネリックプログラミングのHaskellに対するアプローチである (Lämmel and Peyton Jones, 2003)。このアプローチはHaskellのGHC>=6.0の実装でサポートされる。このアプローチを使うことで、ジェネリックな読み込み、ジェネリックな明示、ジェネリックな比較(つまりgread、gshow、geq)と同様に、横断スキーム(例えばいつでもどこでも)のようなジェネリック関数をプログラマーは記述できる。このアプローチはタイプセーフなキャストとコンストラクタアプリケーションの実行のための一部の基本要素に基づいている。
C#と.NETのジェネリックプログラミング
C#(およびその他の.NET言語)のジェネリクスは.NET Framework 2.0の一部として2005年11月に追加された。Javaと似てはいるが、.NETのジェネリクスは、コンパイラによるジェネリクス型から非ジェネリクス型へのコンバートとしてではなく、実行時に実装される。このことにより、ジェネリクス型に関するあらゆる情報はメタデータとして保存される。
.NETジェネリクスの機能
- 型情報を削除せず、CLRの内部でジェネリクスが構築されるため(そしてコンパイラ上では全く構築しないため)、キャストや動的チェックの実行からくるパフォーマンスヒットがない。また、プログラマーはリフレクションを通じてジェネリック情報にアクセスできる。
- 型情報を削除しないので、Javaでは不可能なジェネリック型の配列の生成が可能。
- ジェネリック型の引数として参照型だけでなく値型(組み込みの基本型、およびユーザー定義型の両方)も利用できる。値型の場合、JITコンパイラは特殊化のためにネイティブコードの新しいインスタンスを作成する。このことによりボックス化をする必要がなくなり、パフォーマンスが向上する。
- Javaと同様、ジェネリック型引数がそれら自身のジェネリック型であるようにできる。つまり、
List<List<Dictionary<int, int>>>
のような型は有効である。 - C#(および一般の.NET)は、キーワード
where
を使用することで、値型/参照型、デフォルトコンストラクタの存在、親クラス、実装するインターフェイスなどでジェネリック型を制約することができる。 - 共変性と反変性をサポートしている。C# 4.0以降ではout修飾子またはin修飾子により、型パラメータを共変または反変にすることができる。これによって、ジェネリック型の代入と使用の柔軟性が向上する。
using System;
using System.Collections.Generic;
static int FirstIndexOfMax<T>(List<T> list) where T: IComparable<T>
{
if (list.Count == 0) {
return -1;
}
int index = -1;
for (int i = 0; i < list.Count; ++i) {
if ((index == -1 && list[i] != null) ||
(index >= 0 && list[index] != null && list[i] != null && list[index].CompareTo(list[i]) < 0)) {
index = i;
}
}
return index;
}
この例ではFirstIndexOfMax
メソッドの型パラメータT
に対して、IComparable<T>
インターフェイスを実装していなければならないという制約を指定している。このことにより、IComparable<T>
インターフェイスのメンバであるCompareTo
メソッドが利用可能になっている。
C++/CLIは.NETのジェネリクスとC++のテンプレート両方をサポートする。ただしこれらの間に互換性はない。
その他の言語のジェネリックプログラミング機能
数多くの関数型言語はパラメータ化された型 (parameterized types) とパラメータ多相 (parametric polymorphism) の形で小規模なジェネリックプログラミングをサポートする。さらに標準MLとOCamlはクラステンプレートとAdaのジェネリックパッケージに似たファンクタを提供する。
Verilogのモジュールは1つ以上のパラメタを取ることができる。パラメタの実際の値は、そのモジュールを実体化する際に与えられる。一例としてジェネリックなレジスタアレイがあり、アレイの幅がパラメタで与えられている。そのようなアレイをジェネリックなワイヤベクトルと組み合わせることにより、単一のモジュール実装を用いて任意のビット幅を持つジェネリックなバッファやメモリを作ることができる。[3]
脚注
- ^ Stanley B. Lippman. “Pure C++:Generic Programming Under .NET”. マイクロソフト・MSDNマガジン. 2008年12月28日閲覧。
- ^ 統一モデリング言語 (UML) の用語では、それぞれ汎化 (generalization) および特化 (specialization) と呼ぶ。
- ^ Verilog by Example, Section The Rest for Reference. Blaine C. Readler, Full Arc Press, 2011. ISBN 978-0-9834973-0-1