型クラスの何が問題になっていますか?
-
06-07-2019 - |
質問
型クラスは、非常に一貫性のある汎用的な再利用可能な関数を作成する大きな可能性があるようです。 、効率的で拡張可能な方法。しかし、まだいいえ" mainstream-language"それらを提供します-それどころか:概念は非常に類似しています次のC ++から除外されました!
型クラスに対する理由は何ですか?どうやら多くの言語が同様の問題に対処する方法を探しているようです:.NETは、
T Max<T>(T a, T b) where T : IComparable<T> { // }
インターフェースを実装するすべてのタイプを操作します。
Scalaは代わりに traits と呼ばれる の組み合わせを使用します暗黙的なパラメータ / ビュー境界。これらは自動的に汎用関数に渡されます。
ただし、ここに示す両方の概念には大きな欠点があります-インターフェースは継承ベースであるため、間接性のために比較的遅く、さらに既存の型にそれらを実装させる可能性がありません。
Monoidの抽象化が必要な場合は、インターフェイスを記述して型にこれを実装できますが、 int
などの組み込み型は関数でネイティブに動作できません。
暗黙的なパラメータは、代わりに通常のインターフェイス/特性と矛盾します。
型クラスでは、問題はありません(擬似コード)
typeclass Monoid of A where
static operator (+) (x : A, y : A) : A
static val Zero : A
end
instance Int of Monoid where
static operator (+) (x : Int, y : Int) : Int = x + y
static val Zero : Int = 0
end
では、なぜ型クラスを使用しないのですか?結局のところ、彼らには深刻な不利益があるのですか?
編集:タイプクラスを構造型、純粋なC ++テンプレート、またはアヒル型と混同しないでください。タイプクラスは、タイプごとに明示的にインスタンス化され、慣例によって満たされるだけではありません。さらに、インターフェースを定義するだけでなく、有用な実装を実行できます。
解決
コンセプトは、委員会が時間内にそれらを正しくすることができるとは思わなかったため、またリリースに不可欠とは見なされなかったため、除外されました。彼らが良い考えだと思わないわけではなく、C ++の表現が成熟しているとは思わないだけです。 http://herbsutter.wordpress.com/2009/07/21/trip-report/
静的型は、関数の要件を満たさないオブジェクトを関数に渡さないようにします。 C ++では、これは非常に大きな問題です。オブジェクトがコードによってアクセスされるとき、それが正しいことを確認することがないためです。
コンセプトは、テンプレートの要件を満たさないテンプレートパラメータを渡さないようにします。ただし、コンパイラがテンプレートパラメータにアクセスする時点で、コンセプトがなくても、正しいことを がすでにチェックしています。サポートしていない方法で使用しようとすると、コンパイラエラー[*]が発生します。テンプレートを使用する重いコードの場合、山括弧でいっぱいの3つの画面が表示される場合がありますが、原則として有益なメッセージです。コンパイルが失敗する前にエラーをキャッチする必要は、実行時の未定義の動作の前にエラーをキャッチする必要よりも緊急ではありません。
概念により、複数のインスタンス化にわたって機能するテンプレートインターフェースを簡単に指定できます。これは重要ですが、複数の呼び出しで機能する関数インターフェイスを指定するよりもはるかに差し迫った問題です。
質問への回答-正式なステートメント「このインターフェイスを実装します」実装の前にインターフェースを作成する必要があるという大きな欠点が1つあります。型推論システムにはありませんが、一般に言語は型を使用してインターフェイス全体を表現できないという大きな欠点があるため、正しい型であると推測されるオブジェクトがありますが、そのタイプに起因するセマンティクス。あなたの言語がインターフェースにまったく対応している場合(特にクラスと一致する場合)、ここでのスタンスを取って不利な点を選択する必要があります。
[*]通常。いくつかの例外があります。たとえば、現在C ++型システムでは、入力イテレータを前方イテレータであるかのように使用することはできません。そのためにはイテレータ特性が必要です。アヒルのタイピングだけでは、歩く、泳ぐ、鳴くオブジェクトを渡すのを止めることはありませんが、綿密な検査で実際にアヒルのようにそれらのことを行うことはありませんし、あなたがそれがそうだと思ったことを知って驚いています;-)
他のヒント
インターフェイスは継承ベースである必要はありません...これは、異なる別個の設計上の決定です。新しい Go 言語にはインターフェースがありますが、継承はありません。たとえば、&quot; aタイプは自動的にすべてのインターフェースを満たしますGo FAQ に記載されているように、メソッドのサブセットを指定します。 Goの最近のリリースで促された、継承とインターフェースに関するSimionatoの夢想は、読む価値があります。
abstractのように、タイプクラスはさらに強力であることに同意します。基本クラスでは、追加の便利なコードを指定できます(そうでなければ、ベースクラスに一致するがX自体を定義しないすべての型に対して他の点で追加のメソッドXを定義します)-ABCsの継承荷物なしインターフェイスから)ほとんど必然的に運ぶ。たとえば、PythonのABCが「信じる」ようにするので、必然的に 彼らが提供する概念化の観点から、継承を伴うこと...しかし、実際には、それらは継承ベースである必要はありません(多くはGoのインターフェースのように特定のメソッドの存在と署名をチェックするだけです)。
言語デザイナー(Pythonの場合はGuidoなど)が「羊服の狼」を選択する理由は何ですか? PythonのABCとして、2002年に私が提案していたより単純なHaskellに似たタイプクラスでは、答えるのが難しい質問です。結局のところ、PythonがHaskellからの概念を借用することに反対するように思われるわけではありません(たとえば、リストの内包表記/ジェネレーター式-Pythonにはここで双対性が必要ですが、Haskellにはありません。Haskellは「怠laz」です)。私が提供できる最良の仮説は、今のところ、ほとんどのプログラマーにとって継承が非常に馴染みがあるため、ほとんどの言語デザイナーは、そのようにキャストすることで簡単に受け入れられると感じているということです(ただし、Goのデザイナーはそうしないことを称賛する必要があります)。
太字で始めましょう: 私はそれを持っていることの動機を完全に理解しており、それに反論する人々の動機を理解することはできません...
必要なのは、非仮想アドホックポリモーフィズムです。
- アドホック:実装は異なる場合があります
- 非仮想:パフォーマンス上の理由から。コンパイル時ディスパッチ
残りは私の意見では砂糖です。
C ++には、すでにテンプレートを介したアドホックなポリモーフィズムがあります。 &quot;概念&quot;ただし、どの種類のアドホックポリモーフィック機能がどのユーザー定義エンティティによって使用されるかが明確になります。
C#にはそれを行う方法がありません。 非仮想的ではないアプローチ:floatのような型が単に「INumeric」のようなものを実装する場合または&quot; IAddable&quot; (...)少なくとも、一般的な最小値、最大値、lerpを記述し、そのクランプ、maprange、bezierに基づいて(...)ただし、高速ではありません。あなたはそれを望んでいません。
これを修正する方法:
とにかく.NETはJITコンパイルも行うため、 List&lt; int&gt;
の場合は List&lt; MyClass&gt;
の場合とは異なるコード(値と参照型の違いによる)を生成します。アドホックポリモーフィックパーツ用に異なるコードを生成するために、それほど多くのオーバーヘッドを追加しないでください。
C#言語には、それを表現する方法が必要です。 1つの方法はあなたがスケッチしたものです。
別の方法は、アドホックなポリモーフィック関数を使用して 関数に型制約を追加することです:
U SuperSquare<T, U>(T a) applying{
nonvirtual operator (*) T (T, T)
nonvirtual Foo U (T)
}
{
return Foo(a * a);
}
もちろん、Fooを使用するBarを実装すると、ますます多くの制約が発生する可能性があります。したがって、定期的に使用するいくつかの制約に名前を付けるメカニズムが必要になる場合があります...しかし、これもまた糖質であり、アプローチする1つの方法は、単にタイプクラスの概念を使用することです...
いくつかの制約に名前を付けることは型クラスを定義するようなものですが、私はそれをある種の略語メカニズムとして見てみたいです-関数型制約の任意のコレクションのシュガー:
// adhoc is like an interface: it is about collecting signatures
// but it is not a type: it dissolves during compilation
adhoc AMyNeeds<T, U>
{
nonvirtual operator (*) T (T, T)
nonvirtual Foo U (T)
}
U SuperSquare<T, U>(T a) applying AMyNeeds<T, U>
{
return Foo(a * a);
}
ある場所で&quot; main&quot ;;すべての型引数が既知であり、すべてが具体的になり、一緒にコンパイルできます。
まだ欠けているのは、異なる実装を作成できないことです。上の例では、ポリモーフィック関数を使用し、全員に知らせます...
実装は、拡張メソッドの方法に従うことができます-任意の時点で任意のクラスに機能を追加する能力において:
public static class SomeAdhocImplementations
{
public nonvirtual int Foo(float x)
{
return round(x);
}
}
mainで次のように書くことができます:
int a = SuperSquare(3.0f); // 3.0 * 3.0 = 9.0 rounded should return 9
コンパイラはすべての「非仮想」をチェックします;アドホック関数。組み込みのfloat(*)演算子と int Foo(float)
の両方を検出するため、その行をコンパイルできます。
もちろん、アドホックなポリモーフィズムには、適切な実装が挿入されるように、コンパイル時のタイプごとに再コンパイルする必要があるという欠点があります。そして、おそらくILはそれをdllに入れることをサポートしていません。とにかく、彼らはとにかくそれに取り組んでいます...
型クラス構造のインスタンス化の実際の必要性はないと思います。 コンパイルで何かが失敗すると、制約のエラーが発生します。または、それらが「アドホック」で区切られている場合、エラーメッセージをコードクロックすると、さらに読みやすくなります。
MyColor a = SuperSquare(3.0f);
// error: There are no ad hoc implementations of AMyNeeds<float, MyColor>
// in particular there is no implementation for MyColor Foo(float)
もちろん、型クラス/「アドホックポリモーフィズムインターフェイス」のインスタンス化も考えられる。エラーメッセージは次のように表示されます。&quot; SuperSquareのAMyNeeds制約が一致していません。 AMyNeedsはStandardNeedsとして利用可能です:AMyNeeds&lt; float、int&gt; MyStandardLib
&quot;で定義されています。
また、実装を他のメソッドと一緒にクラスに配置し、「アドホックインターフェイス」を追加することもできます。サポートされているインターフェースのリストへ。
しかし、特定の言語設計からは独立しています:それらを一方向または他の方法で追加することのマイナス面はありません
型クラスに対する理由は何ですか?
新しい言語機能を検討する場合、コンパイラライターの実装の複雑さは常に懸念事項です。 C ++はすでにその間違いを犯しており、その結果として、長年バグのあるC ++コンパイラに苦しんでいます。
インターフェースは継承ベースであるため、間接性のために比較的低速であり、さらに既存の型にそれらを実装させる可能性はありません
真実ではない。 OCamlの構造的に型付けされたオブジェクトシステムを見てください。例:
# let foo obj = obj#bar;;
val foo : < bar : 'a; .. > -> 'a = <fun>
その foo
関数は、必要な bar
メソッドを提供する任意のタイプのオブジェクトを受け入れます。
MLの高次モジュールシステムと同じです。実際、そのクラスと型クラスの間には正式な同等性さえあります。実際には、型クラスは演算子のオーバーロードなどの小規模な抽象化に適していますが、高次モジュールは、岡崎のキュー上のcatenableリストのパラメーター化などの大規模な抽象化に適しています。
結局のところ、彼らには深刻な不利益がありますか?
独自の例、汎用算術を見てください。 Fcodeは、 INumeric
インターフェイスのおかげで、実際にその特定のケースを既に処理できます。 F#の Matrix
タイプは、そのアプローチを使用しています。
ただし、addのマシンコードを別の関数への動的ディスパッチに置き換えたため、算術演算の速度が大幅に低下しました。ほとんどのアプリケーションでは、それは無駄に遅いです。プログラム全体の最適化を実行することでその問題を解決できますが、それには明らかな欠点があります。さらに、数値の堅牢性のために、 int
と float
の数値メソッドには共通性がほとんどないため、抽象化も実用的ではありません。
質問は必ずあるはずです:型クラスの採用について、誰でも 説得力のあるケースを作成できますか?
ただし、「メインストリーム言語」はまだありません。 [タイプクラス]を提供します。
この質問が出されたとき、これは真実だったかもしれません。今日、HaskellやClojureなどの言語に対する関心が非常に高まっています。 Haskellには型クラス( class
/ instance
)があり、Clojure 1.2+にはプロトコルがあります( defprotocol
/ extend
)。
[type classes]に対する理由は何ですか?
型クラスが客観的に「悪い」とは思わない他の多型メカニズムより;彼らはただ別のアプローチに従います。本当の質問は、特定のプログラミング言語にうまく適合するかどうかです。
型クラスがJavaやC#などの言語のインターフェイスとどのように異なるかを簡単に考えてみましょう。これらの言語では、クラスは、そのクラスの定義で明示的に言及および実装されているインターフェースのみをサポートします。ただし、型クラスは、別のモジュールであっても、定義済みの型に後で追加できるインターフェイスです。この種の型の拡張性は、特定の「メインストリーム」のメカニズムとは明らかにまったく異なります。オブジェクト指向言語。
今、いくつかの主流のプログラミング言語の型クラスを考えてみましょう。
Haskell :この言語には型クラスがあると言う必要はありません。
Clojure :前述のように、Clojureには protocols の形式の型クラスがあります。
C ++ :自分で言ったように、概念はC ++ 11仕様から削除されました。
それどころか:非常に類似した概念であるコンセプトは、次のC ++から除外されました!
私はこの決定に関する議論全体には従いませんでした。私が読んだことから、コンセプトは「まだ準備ができていません」:コンセプトマップについてはまだ議論がありました。ただし、概念は完全に放棄されたわけではなく、次のバージョンのC ++に組み込まれることが期待されています。
C#:言語バージョン3では、C#は本質的にオブジェクト指向と関数型プログラミングパラダイムのハイブリッドになりました。型クラスに概念的に非常に似ている言語に1つの追加が行われました:拡張メソッド。主な違いは、インターフェイスではなく、既存の型に(一見)新しいメソッドをアタッチしていることです。
(拡張メソッドのメカニズムは、Haskellの instance&#8230; where
構文ほどエレガントではありません。拡張メソッドは型に「真に」付加されず、構文変換。しかし、結局のところ、これはそれほど大きな実用的な違いを生むことはありません。)
これがすぐに起こるとは思わない&#8212;言語設計者はおそらく言語に拡張機能 properties を追加すらしないでしょうし、拡張機能インターフェースはそれ以上の一歩を踏み出すでしょう。
( VB.NET :マイクロソフトはしばらくの間C#およびVB.NET言語を「共同開発」してきたため、C#に関する私の声明はVB.NETでも有効です。 。)
Java :Javaの知識はあまりありませんが、C ++、C#、Javaの言語の中で、おそらく「純粋な」ものです。オブジェクト指向言語。型クラスがこの言語に自然に適合する方法がわかりません。
F#:なぜ型クラスがF#に導入されないのか。その説明は、F#が構造型システムではなく、主格を持つという事実に集中しています。 (これがF#が型クラスを持たない十分な理由であるかどうかはわかりませんが。)
マトロイドを定義してみてください。これは私たちが行うことです(口語ではなく口頭でマトロイドと言います)。それでもC構造体のようなものです。 リスコフの原則(最新のチューリングメダリスト)は、抽象的すぎ、カテゴリー的すぎ、理論的すぎ、実際の扱いが少なくなります実践的な問題解決のためのデータおよびより純粋な理論的クラスシステム、PROLOGのように見えるコード、コードに関するコードに関するコード...あなたがどの目標を持っているかによって、最小限のコードまたは最も抽象的な問題を解決します。