ジェネリックプログラミングは、後で指定するデータ型でアルゴリズムを記述し、パラメータとして提供される特定の型に対して必要に応じてインスタンス化するコンピュータプログラミングのスタイルです。 1973年にプログラミング言語MLで開拓されたこのアプローチ[ 1 ] [ 2 ]は、使用時に操作する型のセットのみが異なる共通の関数またはデータ型を記述することを可能にし、重複コードを削減します。
ジェネリックプログラミングは、 1977年にAdaによって主流に導入されました。C ++のテンプレートにより、ジェネリックプログラミングはプロのライブラリ設計のレパートリーの一部となりました。これらの技術はさらに改良され、1994年の影響力のある書籍『デザインパターン』でパラメータ化された型が導入されました。[ 3 ]
アンドレイ・アレクサンドレスクは、 2001年に出版した著書『Modern C++ Design : Generic Programming and Design Patterns Applied』の中で、新しい手法を紹介した。その後、D言語も同様のアイデアを実装した。
このようなソフトウェア要素は、Ada、C#、Delphi、Eiffel、F#、Java、Mojo、Nim、Python、Go、Rust、Swift、TypeScript、およびVisual Basic (.NET)ではジェネリクスと呼ばれています。ML 、Scala、Julia、およびHaskellではパラメトリック多相性と呼ばれています。(Haskell の用語では、関連するもののやや異なる概念に対してもジェネリックという用語が使われています。)
ジェネリックプログラミングという用語は、元々はDavid MusserとAlexander Stepanov [ 4 ]によって、上記よりもさらに具体的な意味で造語されました。これは、アルゴリズムやデータ構造の具体的な例からデータ型に関する基本的な要件を抽象化し、概念として形式化するプログラミングパラダイムを説明するもので、これらの概念に基づいてジェネリック関数が実装され、通常は上記で説明した言語のジェネリック性メカニズムが使用されます。
ジェネリックプログラミングは、Musser & Stepanov (1989)で次のように定義されています。
ジェネリックプログラミングは、具体的で効率的なアルゴリズムを抽象化することで、さまざまなデータ表現と組み合わせることができ、多種多様な有用なソフトウェアを生み出すことができる汎用的なアルゴリズムを得るという考え方を中心としている。
— Musser, David R.; Stepanov, Alexander A., Generic Programming [ 5 ]
「ジェネリックプログラミング」パラダイムは、ソフトウェア分解のアプローチであり、アルゴリズムとデータ構造の具体的な例から型に関する基本的な要件を抽象化し、抽象代数における代数理論の抽象化と同様に概念として形式化します。[ 6 ]このプログラミングアプローチの初期の例は Scheme と Ada で実装されましたが、[ 7 ]最もよく知られている例はStandard Template Library (STL) です。[ 8 ] [ 9 ]これは、シーケンスデータ構造とそれらを操作するアルゴリズムを分離するために使用されるイテレータの理論を開発しました。
例えば、N個のシーケンスデータ構造(単方向連結リスト、ベクトルなど)と、それらを操作するM 個のfindアルゴリズム(sortなど)が与えられた場合、直接的なアプローチでは、各データ構造ごとに各アルゴリズムを個別に実装することになり、N × M通りの組み合わせを実装する必要があります。しかし、汎用プログラミングのアプローチでは、各データ構造はイテレータ概念のモデル(現在の値を取得するために逆参照したり、シーケンス内の別の値を指すように変更したりできる単純な値型)を返し、各アルゴリズムは、処理する部分シーケンスまたは範囲の開始と終了を指すイテレータのペアなど、そのようなイテレータを引数として汎用的に記述されます。したがって、実装する必要があるのはN + M 個のデータ構造とアルゴリズムの組み合わせだけです。 STL ではいくつかのイテレータの概念が規定されており、それぞれがより制限的な概念の改良版となっています。たとえば、前方イテレータはシーケンス内の次の値への移動のみを提供します (単方向連結リストや入力データのストリームに適しています)。一方、ランダムアクセスイテレータは、シーケンスの任意の要素への定数時間での直接アクセスも提供します (ベクトルに適しています)。重要な点は、データ構造は効率的に実装できる最も一般的な概念のモデルを返すということです。計算複雑性の要件は、概念定義に明示的に含まれています。これにより、特定のアルゴリズムを適用できるデータ構造が制限され、このような複雑性の要件はデータ構造の選択の主要な決定要因となります。ジェネリックプログラミングは、グラフアルゴリズムなど、他の分野でも同様に適用されています。[ 10 ]
このアプローチはコンパイル時の汎用性やテンプレートといった言語機能を利用することが多いが、特定の言語技術的な詳細には依存しない。汎用プログラミングの先駆者であるアレクサンダー・ステパノフは次のように書いている。
ジェネリックプログラミングとは、アルゴリズムとデータ構造を抽象化し分類することである。その着想は型理論ではなく、クヌースに由来する。その目標は、有用で効率的かつ抽象的なアルゴリズムとデータ構造の体系的なカタログを段階的に構築することである。しかし、そのような取り組みは未だ夢物語に過ぎない。
私は、反復理論は、数学における環論やバナッハ空間論と同様に、コンピュータ科学において極めて重要なものであると信じています。
—アレクサンダー・ステパノフ、『A.ステパノフとのインタビュー』[ 13 ]
ビャルネ・ストロヴストルップは次のように述べた。
Following Stepanov, we can define generic programming without mentioning language features: Lift algorithms and data structures from concrete examples to their most general and abstract form.
—Bjarne Stroustrup, Evolving a language in and for the real world: C++ 1991-2006[12]
Other programming paradigms that have been described as generic programming include Datatype generic programming as described in "Generic Programming – an Introduction".[14] The Scrap your boilerplate approach is a lightweight generic programming approach for Haskell.[15]
In this article we distinguish the high-level programming paradigms of generic programming, above, from the lower-level programming language genericity mechanisms used to implement them (see Programming language support for genericity). For further discussion and comparison of generic programming paradigms, see.[16]
Genericity facilities have existed in high-level languages since at least the 1970s in languages such as ML, CLU and Ada, and were subsequently adopted by many object-based and object-oriented languages, including BETA, C++, D, Eiffel, Java, and DEC's now defunct Trellis-Owl.
ジェネリシティは様々なプログラミング言語で異なる方法で実装され、サポートされています。「ジェネリック」という用語も、様々なプログラミングの文脈で異なる意味で使われてきました。例えば、Forthではコンパイラがコンパイル中にコードを実行でき、コンパイラのキーワードやそれらのキーワードの実装をその場で作成できます。コンパイラの動作を公開するキーワードが少ないため、自然にジェネリシティ機能を提供しますが、ほとんどのForthのテキストではジェネリシティとは呼ばれていません。同様に、動的型付け言語、特にインタプリタ型言語では、関数への値の受け渡しと値の代入の両方が型に依存しないため、通常はデフォルトでジェネリシティが提供されます。このような動作は抽象化やコードの簡潔化によく使用されますが、これは言語が採用している動的型付けシステムの直接的な結果であるため、通常はジェネリシティとは呼ばれません。この用語は関数型プログラミング、特にHaskellのような言語で使用されてきました。これらの言語は構造型システムを使用しており、型は常にパラメトリックで、それらの型に関する実際のコードはジェネリックです。これらの使用法は、コードの節約と抽象化の実現という同様の目的を果たしています。
配列と構造体は、あらかじめ定義された汎用型とみなすことができます。配列型または構造体型を使用するたびに、新しい具象型がインスタンス化されるか、以前にインスタンス化された型が再利用されます。配列要素型と構造体要素型はパラメータ化された型であり、対応する汎用型をインスタンス化するために使用されます。これらはすべて通常コンパイラに組み込まれており、構文は他の汎用構造とは異なります。拡張可能なプログラミング言語の中には、組み込みの汎用型とユーザー定義の汎用型を統一しようとするものがあります。
プログラミング言語における汎用性メカニズムの広範な調査が続く。汎用プログラミングへのメカニズムの適合性を比較した具体的な調査については、[ 17 ]を参照のこと。
静的型付け言語でコンテナクラスを作成する場合、含まれるデータ型ごとに個別の実装を記述するのは不便です。特に、各データ型のコードが実質的に同一である場合はなおさらです。例えば、C++では、クラステンプレートを定義することで、このようなコードの重複を回避できます。
class Animal { // ... };class Car { // ... };template < typename T > class MyList { // クラスの内容。};MyList <動物>動物リスト; MyList <車> carList ;上記は、Tリスト作成時に指定される型を表すプレースホルダーです。これらの「型の汎用オブジェクトT」(一般にテンプレートと呼ばれる)は、サブタイプやシグネチャなどの特定の契約が維持されている限り、クラスを異なるデータ型で再利用できるようにします。この汎用性メカニズムは、交換可能なサブクラスのアルゴリズム的使用法である包含多態性とは混同しないでください。たとえば、型と型のオブジェクトを含む型のオブジェクトのリストなどです。テンプレートは、以下のスワッピングの例のように、型に依存しない関数にも使用できます。MovingEntityAnimalCar
import std ;std :: stringを使用します。// 同様の機能だが、より安全で、場合によってはより高速な関数が // 標準ライブラリのヘッダーに定義されています。 <utility> template < typename T > void swap ( T & a , T & b ) noexcept { T temp = b ; b = a ; a = temp ; }int main ( int argc , char * argv []) { string world = "World!" ; string hello = "Hello," ; swap ( world , hello ); std :: println ( "{} {}" , world , hello ); // 出力は "Hello, World!"です。}template上記で使用したC++の構文は、プログラマーや言語設計者の間でジェネリシティの概念を普及させたジェネリシティ構文として広く知られており、多くのジェネリシティプログラミングの慣用表現をサポートしています。D言語も、C++の先例に基づきながらも構文を簡略化した、完全にジェネリシティに対応したテンプレートを提供しています。Java言語は、Java Platform, Standard Edition (J2SE) 5.0の導入以来、C++の構文に基づいたジェネリシティ機能を提供しています。
C# 2.0、Oxygene 1.5(旧称 Chrome)、およびVisual Basic (.NET) 2005 には、Microsoft .NET Frameworkバージョン 2.0 以降に存在するジェネリクスのサポートを活用する構造があります。
Ada は1977~1980 年に最初に設計されたときからジェネリクスを備えていました。標準ライブラリはジェネリクスを使用して多くのサービスを提供しています。Ada 2005 では、C++ の標準テンプレートライブラリに触発された包括的なジェネリックコンテナライブラリが標準ライブラリに追加されました。[ 18 ] [ 19 ]
汎用ユニットとは、1つ以上の汎用仮パラメータを受け取るパッケージまたはサブルーチンのことである。[ 20 ]
汎用仮パラメータは、値、変数、定数、型、サブルーチン、あるいは別の指定された汎用ユニットのインスタンスです。汎用仮型の場合、構文は離散型、浮動小数点型、固定小数点型、アクセス(ポインタ)型などを区別します。一部の仮パラメータにはデフォルト値を設定できます。[ 21 ]
汎用ユニットをインスタンス化するには、プログラマは各仮引数に実際の引数を渡します。すると、汎用インスタンスは他のユニットと同じように動作します。汎用ユニットは、例えばループ内で実行時にインスタンス化することも可能です。 [ 21 ]
汎用パッケージの仕様:
generic Max_Size : Natural ; -- 汎用的な形式値型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 ;スタック終了;汎用パッケージをインスタンス化する:
type Bookmark_Typeは新しい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 ;言語構文では、汎用的な形式パラメータに対する制約を正確に指定できます。たとえば、汎用的な形式型が実数としてモジュラー型のみを受け入れるように指定できます。また、汎用的な形式パラメータ間の制約を表現することも可能です。たとえば、次のようになります。
汎用型Index_Typeは(<>); -- 離散型である必要がありますtype Element_Typeはprivate ; -- 制限のない任意の型にすることができますtype Array_TypeはElement_Typeの配列( Index_Type範囲<>)です;この例では、は と のArray_Type両方の制約を受けます。ユニットをインスタンス化する際、プログラマはこれらの制約を満たす実際の配列型を渡す必要があります。Index_TypeElement_Type
このきめ細かい制御の欠点は構文が複雑になることですが、すべての汎用的な仮パラメータが仕様書内で完全に定義されているため、コンパイラは汎用型の本体を参照することなく汎用型をインスタンス化できます。
C++とは異なり、Adaでは特殊化されたジェネリックインスタンスは許可されておらず、すべてのジェネリックは明示的にインスタンス化する必要があります。これらの規則にはいくつかの影響があります。
C++ はテンプレートを使用して汎用プログラミング手法を実現します。C++ 標準ライブラリには、一般的なデータ構造やアルゴリズムのテンプレートのフレームワークを提供する標準テンプレートライブラリ(STL) が含まれています。C++ のテンプレートは、テンプレートメタプログラミングにも使用できます。これは、コードの一部を実行時ではなくコンパイル時に事前評価する方法です。テンプレートの特殊化を使用することで、C++ テンプレートはチューリング完全になります。
テンプレートには多くの種類がありますが、最も一般的なのは関数テンプレートとクラステンプレートです。関数テンプレートは、インスタンス化時に指定されたパラメータ型に基づいて通常の関数を作成するためのパターンです。たとえば、C++標準テンプレートライブラリには、またはのいずれか大きい方をstd::max(x, y)返す関数を作成する関数テンプレートが含まれています。は次のように定義できます。xymax()
template < typename T > [[ nodiscard ]] constexpr T max ( T x , T y ) noexcept { return x < y ? y : x ; }この関数テンプレートの特殊化、つまり特定の型を持つインスタンスは、通常の関数と同じように呼び出すことができます。
std :: println ( "{}" , max ( 3 , 7 )); // 出力は7になります。コンパイラは、呼び出しに使用された引数を調べmax、これがへの呼び出しであると判断します。次に、型変数がであるmax(int, int)バージョンの関数をインスタンス化し、次の関数と同等のものを作成します。Tint
[[ nodiscard ]] constexpr int max ( int x , int y ) noexcept { return x < y ? y : x ; }これは、引数xとyが整数、文字列、または式が意味を持つその他の型であるx < yかどうかにかかわらず機能します。より具体的には、が定義されている任意の型に対して機能しますoperator<。使用できる型のセットには共通の継承は必要ないため、ダックタイピングと非常によく似ています。カスタムデータ型を定義するプログラムは、演算子オーバーロードを<使用してその型の意味を定義できるため、std::max()関数テンプレートで使用できます。この孤立した例ではこれは小さな利点のように見えるかもしれませんが、STL のような包括的なライブラリのコンテキストでは、プログラマは、いくつかの演算子を定義するだけで、新しいデータ型に対して広範な機能を取得できます。を定義するだけで、<型を標準のstd::sort()、、std::stable_sort()およびアルゴリズムで使用したり、 (、、ヒープ、および連想配列std::binary_search()に相当する)などのデータ構造内に配置したりできます。std::set<T>TreeSet<T>
C++ テンプレートはコンパイル時に完全に型安全です。例として、標準型では演算子std::complex<T>が定義されていません。これは、複素数<には厳密な順序がないためです。したがって、とが値の場合、コンパイルエラーが発生します。同様に、に依存する他のテンプレートは、比較(ファンクタまたは関数の形式)が提供されない限り、データに適用できません。たとえば、比較が提供されない限り、 (と同等の)のキーとしては使用できません。残念ながら、コンパイラは従来、この種のエラーに対してやや難解で長く、役に立たないエラー メッセージを生成します。特定のオブジェクトがメソッド プロトコルに準拠するようにすることで、この問題を軽減できます。の代わりにを使用する言語では、値をキーとして使用することもできます。std::max(x, y)xycomplex<complexstd::complex<T>std::map<K, V>TreeMap<K, V>std::compare<std::complex<T>
もう一つの種類のテンプレートであるクラステンプレートは、同じ概念をクラスに拡張したものです。クラステンプレートの特殊化はクラスです。クラステンプレートは、汎用コンテナを作成するためによく使用されます。たとえば、STLには二重リンクリストコンテナstd::list<T>( と同等LinkedList<T>)があります。整数のリンクリストを作成するには と書き、list<int>文字列のリストは と表記しますlist<string>。std::list<T>には、互換性のあるパラメータ化型であればどれでも機能する一連の標準関数が関連付けられています。
C++20では、概念を使用してテンプレート型を制約する方法が導入されました。制約はmax()次のようになります。
import std ;std :: totally_orderedを使用します。// 型名の宣言内: template < totally_ordered T > [[ nodiscard ]] constexpr T max ( T x , T y ) noexcept { return x < y ? y : x ; }// requires句内: template < typename T > requires totally_ordered < T > [[ nodiscard ]] constexpr T max ( T x , T y ) noexcept { return x < y ? y : x ; }C++のテンプレートの強力な機能の一つに、テンプレート特殊化があります。これは、インスタンス化されるパラメータ化された型の特定の特性に基づいて、代替の実装を提供することを可能にします。テンプレート特殊化には、特定の形式の最適化を可能にすることと、コードの肥大化を抑制することという2つの目的があります。
例えば、sort()テンプレート関数を考えてみましょう。このような関数が行う主な処理の一つは、コンテナの2つの位置にある値を交換することです。値が大きい場合(それぞれの値を格納するのに必要なバイト数が大きい場合)、最初にオブジェクトへのポインタのリストを別途作成し、それらのポインタをソートしてから、最終的なソート済みシーケンスを作成する方が速い場合がよくあります。しかし、値が非常に小さい場合は、必要に応じて値をその場で交換するのが通常最も速いです。さらに、パラメータ化された型が既に何らかのポインタ型である場合は、別途ポインタ配列を作成する必要はありません。テンプレートの特殊化により、テンプレート作成者は異なる実装を作成し、各実装を使用するためにパラメータ化された型が持つべき特性を指定できます。
関数テンプレートとは異なり、クラステンプレートは部分的に特殊化できます。つまり、テンプレートパラメータの一部が既知の場合にクラステンプレートコードの代替バージョンを提供し、他のテンプレートパラメータは汎用のままにすることができます。たとえば、パラメータ化型をコピーするとコストがかかることを前提としたデフォルトの実装(プライマリ特殊化)を作成し、コピーコストが低い型に対して部分特殊化を作成することで、全体的な効率を向上させることができます。このようなクラステンプレートのクライアントは、コンパイラがプライマリ特殊化を使用したか、部分特殊化を使用したかをそれぞれ知る必要なく、その特殊化を使用するだけです。クラステンプレートは完全に特殊化することもできます。つまり、すべてのパラメータ化型が既知の場合に代替実装を提供できます。
テンプレートの一部の使用例(関数などmax())は、以前は関数のようなプリプロセッサマクロ( C言語の名残)によって処理されていました。例えば、そのようなマクロの実装例を以下に示します。
#define max(a,b) ((a) < (b) ? (b) : (a))マクロは、プログラムのコンパイル前にプリプロセッサによって展開(コピー&ペースト)されます。テンプレートは、実際の関数です。マクロは常にインラインで展開されますが、テンプレートもコンパイラが適切と判断した場合はインライン関数として使用できます。
しかし、これらの目的においては、テンプレートは一般的にマクロよりも優れていると考えられています。テンプレートは型安全です。テンプレートは、関数型マクロを多用するコードによく見られるエラー(副作用のあるパラメータを2回評価するなど)を回避します。おそらく最も重要なのは、テンプレートはマクロよりもはるかに大規模な問題に適用できるように設計されている点です。
テンプレートの使用には主に4つの欠点があります。サポートされている機能、コンパイラのサポート、不十分なエラーメッセージ(通常はC++20以前の置換失敗はエラーではない(SFINAE))、およびコードの肥大化です。
では、テンプレートの使用によって生じるコードの重複問題を軽減するために、派生を利用できるでしょうか?これは、通常のクラスからテンプレートを派生させることを意味します。この手法は、実際の使用においてコードの肥大化を抑制するのに効果的であることが証明されています。このような手法を使用しない場合、中規模のプログラムであっても、重複したコードによって数メガバイトものコード領域が消費されることがわかっています。
—ビャルネ・ストロヴストルップ、『C++の設計と進化』、1994年[ 23 ]
テンプレートによって生成される余分なインスタンス化は、一部のデバッガーがテンプレートを適切に処理できない原因となる場合もあります。例えば、ソースファイルからテンプレート内にデバッグブレークポイントを設定すると、目的のインスタンス化箇所にブレークポイントが設定されなかったり、テンプレートがインスタンス化されるすべての箇所にブレークポイントが設定されたりする可能性があります。
また、テンプレートの実装ソースコードは、それを使用する翻訳単位(ソースファイル)から完全に利用可能でなければなりません(例えば、ヘッダーファイルに含めるなど)。標準ライブラリの多くの部分を含むテンプレートは、ヘッダーファイルに含まれていない場合、コンパイルできません。(これは、バイナリにコンパイルでき、使用するコードに宣言ヘッダーファイルのみを提供する非テンプレートコードとは対照的です。)実装コードが公開されることで一部の抽象化が失われるため、これはデメリットとなる可能性があり、クローズドソースプロジェクトでの使用が制限される可能性があります。
D言語は、 C++の設計に基づいたテンプレートをサポートしています。ほとんどのC++テンプレートの慣用表現は、D言語でも変更なしで動作しますが、D言語にはいくつかの機能が追加されています。
static ifステートメントは、それぞれ C++ のC++ の概念との代替手段を提供しますif constexpr。is(...)式により、コンパイル時にオブジェクトの特性を検証するための投機的インスタンス化が可能になります。autoと式により、変数宣言と関数戻り値の型推論typeofが可能になり、その結果「ヴォルデモート型」(グローバル名を持たない型)が可能になります。[ 24 ]D言語のテンプレートはC++とは異なる構文を使用します。C++ではテンプレートパラメータを山括弧(Template<param1, param2>)で囲みますが、D言語では感嘆符と丸括弧を使用しますTemplate!(param1, param2)。これにより、比較演算子による曖昧さから生じるC++の構文解析の困難さを回避できます。パラメータが1つしかない場合は、丸括弧を省略できます。
従来、D言語は上記の機能を組み合わせて、トレイトベースのジェネリックプログラミングを使用してコンパイル時の多相性を実現しています。例えば、入力範囲は、によって実行されるチェックを満たす任意の型として定義されisInputRange、それは次のように定義されます。
template isInputRange ( R ) { enum bool isInputRange = is ( typeof (( inout int = 0 ) { R r = R . init ; // 範囲オブジェクトを定義できますif ( r . empty ) {} // 空かどうかをテストできますr . popFront (); // popFront() を呼び出すことができますauto h = r . front ; // 範囲の先頭を取得できます})); }入力範囲のみを受け入れる関数は、上記のテンプレートをテンプレート制約内で使用できます。
auto fun ( Range )( Range range ) if ( isInputRange ! Range ) { // ... }テンプレートメタプログラミングに加えて、D言語はコンパイル時のコード生成を可能にするためのいくつかの機能も提供しています。
import式を使うと、ディスクからファイルを読み込み、その内容を文字列式として使用できます。上記を組み合わせることで、既存の宣言に基づいてコードを生成できます。例えば、D言語のシリアライゼーションフレームワークは、型のメンバーを列挙し、シリアライズおよびデシリアライズを実行するための専用関数を各シリアライズ対象型ごとに生成できます。ユーザー定義属性によって、さらにシリアライゼーション規則を指定することも可能です。
式importとコンパイル時の関数実行により、ドメイン固有言語を効率的に実装することも可能です。例えば、HTMLテンプレートを含む文字列を受け取り、それと同等のD言語ソースコードを返す関数がある場合、次のように使用できます。
// example.htt の内容を文字列マニフェスト定数としてインポートします。enum htmlTemplate = import ( "example.htt" );// HTML テンプレートを D コードにトランスパイルします。enum htmlDCode = htmlTemplateToD ( htmlTemplate );// htmlDCode の内容を D コードとして貼り付けます。mixin ( htmlDCode );ジェネリッククラスは、 Eiffelのオリジナルのメソッドと言語設計の頃から存在しています。Eiffel の基礎となる出版物[ 25 ] [ 26 ]では、ジェネリッククラスの作成と使用を説明するためにジェネリシティという用語を使用しています。
ジェネリッククラスは、クラス名と1つ以上の仮ジェネリックパラメータのリストで宣言されます。次のコードでは、クラスにLISTは1つの仮ジェネリックパラメータがあります。G
class LIST [ G ] ... feature -- アクセスitem : G -- カーソルが現在指している項目... feature -- 要素の変更put ( new_item : G ) -- リストの末尾に `new_item` を追加...形式的なジェネリックパラメータは、ジェネリッククラスの宣言時に指定される任意のクラス名のプレースホルダーです。以下の 2 つのジェネリック派生式に示されており、ACCOUNTおよびDEPOSITは他のクラス名です。ACCOUNTおよび は、実際の使用時に の代わりに使用される実際のクラス名を提供するので、実際のジェネリックパラメータDEPOSITとみなされます。G
list_of_accounts : LIST [ ACCOUNT ] -- アカウント一覧list_of_deposits : LIST [ DEPOSIT ] -- 預金リストEiffel型システムでは、クラスLIST [G]はクラスとみなされますが、型とはみなされません。しかし、クラスのジェネリック派生であるLIST [G]などはLIST [ACCOUNT]型とみなされます。
上記のリストクラスでは、 の代わりに実際のジェネリックパラメータとして使用できるGクラスは、他の利用可能なクラスです。有効な実際のジェネリックパラメータを選択できるクラスのセットを制限するには、ジェネリック制約を指定できます。以下のクラスの宣言ではSORTED_LIST、ジェネリック制約により、有効な実際のジェネリックパラメータは、クラス を継承するクラスである必要がありますCOMPARABLE。ジェネリック制約により、 の要素がSORTED_LIST実際にソートできることが保証されます。
クラスSORTED_LIST [ G -> COMPARABLE ]ジェネリクス、つまり「型 T のコンテナ」のサポートは、2004 年に J2SE 5.0 の一部としてJava プログラミング言語に追加されました。Java では、ジェネリクスはコンパイル時にのみ型の正しさがチェックされます。ジェネリクスの型情報は、古いJVM実装との互換性を維持するために、型消去と呼ばれるプロセスによって削除され、実行時には使用できなくなります。[ 27 ]例えば、a は生の型に変換されます。コンパイラは、リストから要素を取得するときに、要素を型に変換する型キャストを挿入しますが、C++ テンプレートなどの他の実装と比較してパフォーマンスが低下します。List<String>java.util.Listjava.lang.String
ジェネリクスは、1999 年に Microsoft Research が開始した研究プロトタイプに基づいて、2005 年 11 月に.NET Framework 2.0の一部として追加されました。 [ 28 ] Java のジェネリクスと似ていますが、.NET のジェネリクスは型消去を適用せず、[ 29 ] : 208–209実行時に再具体化を使用してジェネリクスを第一級のメカニズムとして実装します。この設計上の選択により、ジェネリック型の保持を伴うリフレクションの許可や、消去の制限 (ジェネリック配列を作成できないなど) の一部の緩和など、追加の機能が提供されます。[ 30 ] [ 31 ]これはまた、実行時のキャストや通常はコストのかかるボックス化変換によるパフォーマンスの低下がないことを意味します。プリミティブ型と値型がジェネリック引数として使用される場合、それらは特殊な実装を取得し、効率的なジェネリックコレクションとメソッドを可能にします。C++ や Java と同様に、ネストされたジェネリック型はDictionary<string, List<int>>有効な型ですが、コード分析設計ルールではメンバシグネチャには推奨されません。[ 32 ]
.NET では、キーワードを使用して 6 種類のジェネリック型制約が可能です。whereこれには、ジェネリック型を値型、クラス、コンストラクター、インターフェースの実装に制限することが含まれます。[ 33 ]以下は、インターフェース制約の例です。
名前空間Wikipedia.Examples ;Systemを使用します。public class Example { static void Main () { int [] a = { 0 , 1 , 2 , 3 }; MakeAtLeast < int > ( a , 2 ); // a を { 2, 2, 2, 3 } に変更foreach ( int i in a ) { Console . WriteLine ( i ); // 結果を出力します。} Console . ReadKey ( true ); }static void MakeAtLeast < T > ( T [] list , T lowest ) where T : IComparable < T > { for ( int i = 0 ; i < list . Length ; i ++ ) { if ( list [ i ]. CompareTo ( lowest ) < 0 ) { list [ i ] = lowest ; } } } }このMakeAtLeast()メソッドは、ジェネリック型の要素を持つ配列に対する操作を可能にしますT。メソッドの型制約は、このメソッドがTジェネリックSystem.IComparable<T>インターフェイスを実装する任意の型に適用可能であることを示しています。これにより、型が比較をサポートしていない場合にメソッドが呼び出されると、コンパイル時エラーが発生します。インターフェイスは、ジェネリックメソッドを提供しますCompareTo(T)。
上記の方法は、ジェネリック型を使用せずに、非ジェネリックSystem.Array型のみを使用して記述することもできます。ただし、配列は反変であるため、型変換は型安全ではなくなり、コンパイラはジェネリック型を使用した場合に検出される可能性のある特定のエラーを検出できなくなります。さらに、この方法では配列の要素をobjects としてアクセスする必要があり、2 つの要素を比較するには型変換が必要になります。( のような値型の場合、このような型ではボックス化int変換が必要になりますが、標準のコレクション クラスで行われているように、クラスを使用することでこの問題を回避できます。)System.Collections.Generic.Comparer<T>
汎用.NETクラスにおける静的メンバーの注目すべき動作は、実行時型ごとに静的メンバーがインスタンス化されることです(以下の例を参照)。
名前空間Wikipedia.Examples ;Systemを使用します。// 汎用クラスclass GenericTest < T > { // 静的変数 - リフレクション時に各型ごとに作成されますstatic CountedInstances OnePerType = new ();// データメンバprivate T _t ;// デフォルトコンストラクタpublic GenericTest ( T t ) { _t = t ; } }// クラスclass CountedInstances { // 静的変数 - インスタンスごとに一度インクリメントされますpublic static int Counter ;// デフォルトコンストラクタpublic CountedInstances () { // オブジェクトのインスタンス化中にカウンターを 1 増やすCountedInstances . Counter ++ ; } }public class GenericExample { static void Main ( string [] args ) { // メインコードのエントリポイント// 実行終了時、CountedInstances.Counter = 2 GenericTest < int > g1 = new ( 1 ); GenericTest < int > g11 = new ( 11 ); GenericTest < int > g111 = new ( 111 ); GenericTest < double > g2 = new ( 1.0 ); } }Pascalでは、ジェネリクスは 2006 年にFree Pascal の実装で初めて実装されました。
Object Pascal の方言であるDelphi は、 CodeGearによる 2007 年の Delphi 11 リリースでジェネリクスを獲得しました。当初は .NET コンパイラ (現在はサポート終了) のみで利用可能でしたが、2009 年の Delphi 12 リリースでネイティブ コードに追加されました。Delphi のジェネリクスの意味論と機能は、.NET 2.0 のジェネリクスをモデルにしていますが、実装は必然的にかなり異なります。以下は、上記の最初の C# の例をほぼそのまま翻訳したものです。
プログラムサンプル;{$APPTYPE CONSOLE}ジェネリクスを使用します。デフォルト; //IComparer<>用type TUtils = class class procedure MakeAtLeast < T > ( Arr : TArray < T >; const Lowest : T ; Comparer : IComparer < T > ) ; overload ; class procedure MakeAtLeast < T > ( Arr : TArray < T >; const Lowest : T ) ; overload ; end ;class procedure TUtils . MakeAtLeast < T > ( Arr : TArray < T >; const Lowest : T ; Comparer : IComparer < T > ) ; var I : Integer ; begin if Comparer = nil then Comparer := TComparer < T >. Default ; for I := Low ( Arr ) to High ( Arr ) do if Comparer . Compare ( Arr [ I ] , Lowest ) < 0 then Arr [ I ] := Lowest ; end ;class procedure TUtils . MakeAtLeast < T > ( Arr : TArray < T >; const Lowest : T ) ; begin MakeAtLeast < T > ( Arr , Lowest , nil ) ; end ;var Ints : TArray < Integer >; Value : Integer ; begin Ints := TArray < Integer >. Create ( 0 , 1 , 2 , 3 ) ; TUtils . MakeAtLeast < Integer > ( Ints , 2 ) ; for Value in Ints do WriteLn ( Value ) ; ReadLn ; end .C#と同様に、メソッドと型全体には1つ以上の型パラメーターを指定できます。この例では、TArrayはジェネリック型(言語で定義)であり、MakeAtLeastはジェネリックメソッドです。使用可能な制約はC#の制約と非常によく似ています。任意の値型、任意のクラス、特定のクラスまたはインターフェース、パラメーターなしのコンストラクタを持つクラスです。複数の制約は加算的な共用体として機能します。
Free Pascalは、Delphiよりも前に、異なる構文と意味論で、2006年のバージョン2.2.0でジェネリクスを実装しました。しかし、FPCバージョン2.6.0以降では、言語モードを使用するとDelphiスタイルの構文が利用可能になりました{$mode Delphi}。したがって、Free Pascalコードはどちらのスタイルのジェネリクスもサポートしています。
DelphiとFree Pascalの例:
// Delphi スタイルのユニットA ;{$ifdef fpc} {$mode delphi} {$endif}インタフェースtype TGenericClass < T > = class function Foo ( const AValue : T ) : T ; end ;実装関数TGenericClass < T >。Foo ( const AValue : T ) : T ;結果: = AValue + AValue ;終わり;終わり。// Free Pascal の ObjFPC スタイルの単位B ;{$ifdef fpc} {$mode objfpc} {$endif}インタフェースtype generic TGenericClass < T > = class function Foo ( const AValue : T ) : T ; end ;実装関数TGenericClass 。Foo ( const AValue : T ) : T ;結果: = AValue + AValue ;終わり;終わり。// 使用例、Delphi スタイルのプログラムTestGenDelphi ;{$ifdef fpc} {$mode delphi} {$endif}A 、Bを使用する。var GC1 : A . TGenericClass < Integer >; GC2 : B . TGenericClass < String >; begin GC1 := A . TGenericClass < Integer >. Create ; GC2 := B . TGenericClass < String >. Create ; WriteLn ( GC1 . Foo ( 100 )) ; // 200 WriteLn ( GC2 . Foo ( 'hello' )) ; // hellohello GC1 . Free ; GC2 . Free ; end .// 使用例、ObjFPC スタイルのプログラムTestGenDelphi ;{$ifdef fpc} {$mode objfpc} {$endif}A 、Bを使用する。// ObjFPC型で必須TAGenericClassInt = specialize A . TGenericClass < Integer >; TBGenericClassString = specialize B . TGenericClass < String >; var GC1 : TAGenericClassInt ; GC2 : TBGenericClassString ; begin GC1 := TAGenericClassInt . Create ; GC2 := TBGenericClassString . Create ; WriteLn ( GC1 . Foo ( 100 )) ; // 200 WriteLn ( GC2 . Foo ( 'hello' )) ; // hellohello GC1 . Free ; GC2 . Free ; end .Haskellの型クラス機構は、ジェネリックプログラミングをサポートしています。Haskell の 6 つの定義済み型クラス (等価性を比較できる型、および値を文字列としてレンダリングできる型を含む) は、派生インスタンスをサポートするという特別な特性を持っています。これは、新しい型を定義するプログラマが、通常クラス インスタンスを宣言する際に必要となるクラス メソッドの実装を提供することなく、この型がこれらの特別な型クラスの 1 つのインスタンスであることを宣言できることを意味します。必要なすべてのメソッドは、型の構造に基づいて「派生」、つまり自動的に構築されます。たとえば、次の二分木の型の宣言は、それがクラスおよびのインスタンスであることを宣言しています。EqShowEqShow
data BinTree a = Leaf a | Node ( BinTree a ) a ( BinTree a ) deriving ( Eq , Show )これにより、等価関数(==)と文字列表現関数( )が、その操作をサポートするshow形式の任意の型に対して自動的に定義されます。BinTree TT
派生インスタンスのサポートにより、メソッドEqとShowメソッドは、パラメトリック多相関数==とshowは質的に異なる方法で汎用的になります。これらの「関数」(より正確には、型インデックス付き関数のファミリー)は、さまざまな型の値に適用でき、引数の型ごとに動作は異なりますが、新しい型のサポートを追加するために必要な作業はごくわずかです。Ralf Hinze (2004) は、特定のプログラミング手法によって、ユーザー定義型クラスに対しても同様の効果が得られることを示しました。他の研究者は、Haskell および Haskell の拡張 (後述) のコンテキストで、これやその他の種類の汎用性に対するアプローチを提案しています。
PolyPは、 Haskellに対する最初の汎用プログラミング言語拡張です。PolyPでは、汎用関数は多型関数と呼ばれます。この言語では、このような多型関数を、正規データ型のパターンファンクタの構造に対する構造帰納法によって定義できる特別な構造が導入されています。PolyPの正規データ型は、Haskellのデータ型のサブセットです。正規データ型tは、型が* → *でなければならず、定義においてaが仮型引数である場合、tへのすべての再帰呼び出しはtaの形式を持たなければなりません。これらの制約により、再帰呼び出しの形式が異なる高階型データ型やネストされたデータ型は除外されます。PolyPのflatten関数を例として以下に示します。
flatten :: Regular d => d a -> [ a ] flatten = cata flpolytypic 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 -> bGeneric Haskellは、オランダのユトレヒト大学で開発されたHaskellの拡張機能の一つです。Generic Haskellが提供する拡張機能は以下のとおりです。
結果として得られる型インデックス付き値は、任意の型に特化させることができます。
例として、Generic Haskell の等価関数: [ 34 ]
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 )等式{ | t :: k | } :: Eq {[ k ]} t t 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 { | }内部| } = ( == ) eq { |シャア| } = ( == ) eq { |ブール| } = ( == )Cleanは、GHC ≥ 6.0でサポートされているPolyPとGeneric Haskellに基づいた汎用プログラミングを提供します。それらと同様に種類によるパラメータ化が可能ですが、オーバーロード機能も備えています。
MLファミリーの言語は、パラメトリック多相性とファンクタと呼ばれるジェネリックモジュールを通して、ジェネリックプログラミングをサポートしています。Standard MLとOCamlはどちらもファンクタを提供しており、これはクラステンプレートやAdaのジェネリックパッケージに似ています。Schemeの構文抽象化もジェネリシティと関連があり、実際にはC++テンプレートの上位集合となっています。
Verilogモジュールは1つ以上のパラメータを受け取ることができ、それらの実際の値はモジュールのインスタンス化時に割り当てられます。1つの例として、配列の幅がパラメータによって指定される汎用レジスタ配列があります。このような配列は、汎用ワイヤベクトルと組み合わせることで、単一のモジュール実装から任意のビット幅を持つ汎用バッファまたはメモリモジュールを作成できます。[ 35 ]
VHDLはAdaから派生しているため、汎用的な機能も備えている。[ 36 ]
C_Generic言語には、キーワードを使用した「型ジェネリック式」と呼ばれる機能があります。 [ 37 ]この機能はC言語に関数オーバーロード機能を与えますが、ジェネリックプログラミングとは関係ありません。ジェネリックプログラミングは、プリプロセッサを使用してジェネリックコードを定義し、マクロ展開がインスタンス化の役割を果たすことで実現できます。場合によっては、非標準の拡張機能「ステートメント式」を使用して、ジェネリックコードの関数のような動作をシミュレートします。これら2つの機能を組み合わせることで、 C++のmax関数と同様に、ジェネリックmax関数を実装できます。std::max
#define max(a,b) \ ({ typeof (a) _a = (a); \ typeof (b) _b = (b); \ _a > _b ? _a : _b; })