Haskellを学び始めたとき、型クラスはインターフェースとは異なり、より強力であると言われました。
1年後、私はインターフェイスと型クラスを広範囲に使用しましたが、それらの違いの例や説明はまだ見ていません。それは自然に起こる啓示ではないか、私が明白な何かを見逃したか、実際には実際の違いはありません。
インターネットを検索しても、実質的なものは何も見つかりませんでした。だから、あなたは答えがありますか?
これはさまざまな角度から見ることができます。他の人は同意しませんが、OOPインターフェイスは型クラスを理解するために始めるのに適した場所だと思います(確かに何もないところから始めるのと比較して)。
人々は、概念的に、型クラスは集合のように型を分類することを指摘するのが好きです-「これらの操作をサポートする型のセットと、言語自体ではエンコードできない他の期待」。 「特定の要件を満たしている場合にのみ、型をこのクラスのインスタンスにする」と言って、メソッドなしで型クラスを宣言することは理にかなっており、時々行われます。 OOPインターフェースではめったに起こりません1。
具体的な違いとして、型クラスがOOPインターフェイスよりも強力である方法は複数あります。
最大のものは、型クラスが、型がインターフェースを実装するという宣言を、型自体の宣言から切り離すことです。 OOPインターフェイスでは、型を定義するときに型が実装するインターフェイスを一覧表示します。後で追加する方法はありません。型クラスでは、特定の型の新しい型クラスを作成すると、 " 「モジュール階層の上位」を実装できますが、それを知らない場合は、インスタンス宣言を記述できます。互いに知らない別々のサードパーティの型と型クラスがある場合は、次のインスタンス宣言を記述できます。 OOPインターフェースの類似したケースでは、ほとんどの場合、行き詰まっていますが、OOP言語は「設計パターン」(アダプター)を進化させて、制限。
次に大きいのは(もちろんこれは主観的です)、概念的には、OOPインターフェイスはインターフェイスを実装するオブジェクトで呼び出すことができる一連のメソッドですが、型クラスは一連のメソッドですこれは、クラスのメンバーである型で使用できます。区別は重要です。型クラスのメソッドは、オブジェクトではなく型を参照して定義されるため、型の複数のオブジェクトをパラメーターとして持つメソッドを持つことに支障はありません(等式および比較演算子)、または結果として型のオブジェクトを返す(さまざまな算術演算)、または型の定数(最小および最大境界)さえも返します。OOPインターフェイスは 'これを行うと、OOP言語は、制限を回避するために設計パターン(仮想クローンメソッドなど)を進化させました。
OOPインターフェースは、タイプに対してのみ定義できます。型クラスは、いわゆる「型コンストラクター」に対して定義することもできます。さまざまなC派生OOP言語でテンプレートとジェネリックを使用して定義されたさまざまなコレクション型は型コンストラクターです:リストは引数として型T
を取り、型List<T>
を構成します。型クラスを使用すると、型コンストラクターのインターフェイスを宣言できます。たとえば、コレクションの各要素で提供された関数を呼び出し、コレクションの新しいコピーに結果を収集するコレクション型のマッピング操作(場合によっては異なる要素型を使用)。 OOPインターフェイスではこれを行うことはできません。
特定のパラメーターが複数のインターフェースを実装する必要がある場合、型クラスを使用すると、どのパラメーターをメンバーにする必要があるかを簡単にリストできます。 OOPインターフェイスでは、特定のポインタまたは参照のタイプとして指定できるインターフェイスは1つだけです。さらに実装する必要がある場合は、1つのインターフェイスをで記述するなどの魅力のないオプションしかありません。署名して他のインターフェイスにキャストするか、インターフェイスごとに個別のパラメータを追加して、それらが同じオブジェクトを指すようにする必要があります。型が勝ったため、必要なインターフェイスを継承する新しい空のインターフェイスを宣言して解決することもできません。祖先を実装しているという理由だけで、新しいインターフェイスを実装していると自動的に見なされることはありません(事後に実装を宣言できれば、これはそれほど問題にはなりませんが、そうすることもできません)。
上記の逆の場合のように、2つのパラメーターに、特定のインターフェースを実装するタイプと、同じタイプであるを要求できます。 OOPインターフェースでは、最初の部分しか指定できません。
型クラスのインスタンス宣言はより柔軟です。 OOPインターフェイスでは、「タイプXを宣言しており、インターフェイスYを実装しています」としか言えません。ここで、XとYは特定です。タイプクラスでは、「すべてのリスト」と言うことができます。要素型がこれらの条件を満たす型はYのメンバーです」(Haskellではこれはいくつかの理由で問題がありますが、「XとYのメンバーであるすべての型はZのメンバーでもある」と言うこともできます。)
いわゆる「スーパークラス制約」は、単なるインターフェイスの継承よりも柔軟性があります。 OOPインターフェースでは、「型がこのインターフェースを実装するには、これらの他のインターフェースも実装する必要があります」としか言えません。これは、型クラスでも最も一般的なケースですが、スーパークラスの制約もあります。 「SomeTypeConstructorはまあまあのインターフェースを実装する必要がある」、「型に適用されたこの型関数の結果はまあまあの制約を満たす必要がある」などと言うことができます。
これは現在Haskellの言語拡張です(型関数もそうです)が、複数の型を含む型クラスを宣言することができます。たとえば、同型クラス:情報を失うことなく一方から他方に変換したり元に戻したりできるタイプのペアのクラス。繰り返しますが、OOPインターフェースでは不可能です。
きっともっとあると思います。
ジェネリックスを追加するOOP言語では、これらの制限の一部を消去できることに注意してください(4番目、5番目、おそらく2番目のポイント)。
一方、OOPインターフェイスが実行できることと、型クラスがネイティブに実行できない2つの重要なことがあります。
ランタイム動的ディスパッチ。 OOP言語では、インターフェイスを実装するオブジェクトへのポインタを渡して格納し、実行時にオブジェクトの動的な実行時タイプに従って解決されるメソッドを呼び出すのは簡単です。対照的に、型クラスの制約はデフォルトですべてコンパイル時に決定されます-そしておそらく驚くべきことに、ほとんどの場合、これが必要なすべてです。動的ディスパッチが必要な場合は、いわゆる存在型(現在、Haskellの言語拡張):オブジェクトの型が何であるかを「忘れて」、特定の型クラスの制約に従ったことだけを(オプションで)記憶する構造。その時点から、基本的にまったく同じように動作します。 OOP言語でインターフェイスを実装するオブジェクトへのポインタまたは参照としての方法であり、型クラスにはこの領域に欠陥はありません(同じ型クラスを実装する2つの存在がある場合は、 twを必要とする型クラスメソッドoそのタイプのパラメーターの場合、存在が同じタイプであるかどうかがわからないため、存在をパラメーターとして使用することはできません。しかし、そもそもそのようなメソッドを持つことができないOOP言語と比較すると、これは損失ではありません。)
インターフェイスへのオブジェクトのランタイムキャスト。 OOP言語では、実行時にポインタまたは参照を取得して、インターフェイスが実装されているかどうかをテストし、実装されている場合はそのインターフェイスに「キャスト」できます。型クラスにはネイティブには何もありません。同等(これは、 parametricity というプロパティを保持するため、いくつかの点で利点がありますが、ここでは説明しません)。もちろん、新しい型クラスを追加することを妨げるものは何もありません(または既存のオブジェクトを拡張する)必要な型クラスの存在に型のオブジェクトをキャストするメソッドを使用します(このような機能をライブラリとしてより一般的に実装することもできますが、かなり複雑です。終了してアップロードする予定です。それをハッカゲにいつか、約束します!)
あなたはこれらのことを行うことができますが、多くの人がOOPそのように悪いスタイルをエミュレートすることを検討し、より簡単なソリューションを使用することをお勧めします、型クラスの代わりに関数の明示的なレコードなど。完全なファーストクラス関数を使用すると、そのオプションはそれほど強力ではありません。
運用上、OOPインターフェイスは通常、オブジェクトが実装するインターフェイスの関数ポインタのテーブルを指すポインタをオブジェクト自体に格納することによって実装されます。型クラスは通常実装されます( 「辞書の受け渡し」による(C++のようなマルチインスタンス化による多態性ではなく、Haskellのようなボクシングによる多形性):コンパイラは、関数(および定数)のテーブルへのポインタを、使用する各関数への非表示パラメータとして暗黙的に渡します。型クラスであり、関係するオブジェクトの数に関係なく、関数は1つのコピーを取得します(これが、上記の2番目のポイントで説明したことを実行できる理由です)。存在型の実装は、OOP言語は行います:型クラス辞書へのポインタは、「忘れられた」型がそのメンバーであるという「証拠」としてオブジェクトとともに格納されます。
C++の「概念」提案(元々はC++ 11で提案されていた)について読んだことがあるなら、それは基本的にC++のテンプレート用に再考されたHaskellの型クラスです。 C++-with-conceptsを使用し、オブジェクト指向関数と仮想関数の半分を取り除いて、構文やその他の疣贅をクリーンアップし、実行時型情報が必要な場合に備えて既存の型を追加する言語があればいいと思うことがあります。タイプベースの動的ディスパッチ。 (更新: Rust は基本的にこれであり、他の多くの素晴らしいものもあります。)
1Serializable in Javaは、メソッドまたはフィールドのないインターフェースであるため、これらのまれな発生の1つです。
Haskell型クラスについて話していると思います。これは、実際にはインターフェイスと型クラスの違いではありません。名前が示すように、型クラスは、共通の関数セット(および、TypeFamilies拡張機能を有効にした場合は関連する型)を持つ型のクラスにすぎません。
ただし、Haskellの型システムは、それ自体が、たとえばC#の型システムよりも強力です。これにより、C#では表現できないHaskellで型クラスを記述できます。 Functor
のような単純な型クラスでさえ、C#では表現できません。
class Functor f where
fmap :: (a -> b) -> f a -> f b
C#の問題は、ジェネリック自体をジェネリックにすることができないことです。言い換えると、C#では種類の種類のみ*
はポリモーフィックにすることができます。 Haskellはポリモーフィック型コンストラクターを許可しているので、どんな種類の型もポリモーフィックにすることができます。
これが、Haskellの強力なジェネリック関数の多くが(mapM
、liftA2
など)は、それほど強力でない型システムではほとんどの言語で表現できません。
主な違い(型クラスをインターフェイスよりもはるかに柔軟にする)は、型クラスがそのデータ型から独立しており、追加できることです後で。もう1つの違い(少なくともJavaとの違い)は、デフォルトの実装を提供できることです。例:
//Java
public interface HasSize {
public int size();
public boolean isEmpty();
}
このインターフェースを持つことは素晴らしいことですが、それを変更せずに既存のクラスに追加する方法はありません。運が良ければ、クラスは非ファイナル(たとえば、ArrayList
)なので、そのインターフェイスを実装するサブクラスを作成できます。クラスが最終的な場合(たとえばString
)、運が悪いです。
これをHaskellと比較してください。次の型クラスを記述できます。
--Haskell
class HasSize a where
size :: a -> Int
isEmpty :: a -> Bool
isEmpty x = size x == 0
また、既存のデータ型に触れることなく、クラスに追加できます。
instance HasSize [a] where
size = length
型クラスのもう1つの優れたプロパティは、暗黙的な呼び出しです。例えば。 JavaにComparator
がある場合は、それを明示的な値として渡す必要があります。 Haskellでは、適切なインスタンスがスコープ内に入るとすぐに、同等のOrd
を自動的に使用できます。