型クラスとオブジェクトインターフェイス


33

型クラスを理解しているとは思わない。私はどこかで、型が実装する「インターフェース」(OOから)として型クラスを考えることは間違っており、誤解を招くものだと読みました。問題は、それらを異なるものとみなし、それがどのように間違っているかを見るのに問題があることです。

たとえば、(Haskell構文の)型クラスがある場合

class Functor f where
  fmap :: (a -> b) -> f a -> f b

これはインターフェース[1](Java構文)とどのように違いますか

interface Functor<A> {
  <B> Functor<B> fmap(Function<B, A> fn)
}

interface Function<Return, Argument> {
  Return apply(Argument arg);
}

考えられる違いの1つは、特定の呼び出しで使用される型クラスの実装が指定されず、環境から決定されることです。たとえば、この型の実装で使用可能なモジュールを調べます。これは、OO言語で対処できる実装アーティファクトのようです。コンパイラー(またはランタイム)が、タイプに必要なインターフェースを公開するラッパー/エクステンダー/モンキーパッチャーをスキャンできるように。

私は何が欠けていますか?

[1] オブジェクト指向言語であるため、f a引数が削除されていることに注意してくださいfmap。オブジェクトでこのメソッドを呼び出すことになります。このインターフェイスは、f a引数が修正されていることを前提としています。

回答:


46

基本形式では、型クラスはオブジェクトインターフェイスにやや似ています。ただし、多くの点で、はるかに一般的です。

  1. ディスパッチは値ではなくタイプです。値を実行する必要はありません。たとえば、HaskellのReadクラスのように、関数の結果タイプでディスパッチを実行することができます。

    class Read a where
      readsPrec :: Int -> String -> [(a, String)]
      ...
    

    このようなディスパッチは、従来のオブジェクト指向では明らかに不可能です。

  2. 型クラスは、単に複数のパラメーターを提供するだけで、自然に複数のディスパッチに拡張されます。

    class Mul a b c where
      (*) :: a -> b -> c
    
    instance Mul Int Int Int where ...
    instance Mul Int Vec Vec where ...
    instance Mul Vec Vec Int where ...
    
  3. インスタンス定義はクラス定義とタイプ定義の両方から独立しているため、よりモジュール化されています。モジュールAのタイプTは、モジュールM3にインスタンスを提供するだけで、どちらの定義も変更せずに、モジュールM2のクラスCに後付けできます。OOでは、これには拡張メソッドなどのより難解な(そしてOOっぽくない)言語機能が必要です。

  4. 型クラスは、サブタイプではなく、パラメトリック多型に基づいています。これにより、より正確なタイピングが可能になります。例えば検討してください

    pick :: Enum a => a -> a -> a
    pick x y = if fromEnum x == 0 then y else x
    

    pick(x : Enum, y : Enum) : Enum = if x.fromEnum() == 0 then y else x
    

    前者の場合、適用 pick '\0' 'x'はtypeがCharありますが、後者の場合、結果について知っているのはそれがEnumであることだけです。(これが、最近のほとんどのオブジェクト指向言語がパラメトリック多相性を統合する理由でもあります。)

  5. 密接に関連するのは、バイナリメソッドの問題です。型クラスでは完全に自然です:

    class Ord a where
      (<) :: a -> a -> Bool
      ...
    
    min :: Ord a => a -> a -> a
    min x y = if x < y then x else y
    

    サブタイピングだけでは、Ordインターフェースを表現することは不可能です。正確に行うには、より複雑で再帰的な形式、または「F境界の定量化」と呼ばれるパラメトリック多型が必要です。Java Comparableとその使用を比較します。

    interface Comparable<T> {
      int compareTo(T y);
    };
    
    <T extends Comparable<T>> T min(T x, T y) {
      if (x.compareTo(y) < 0)
        return x;
      else
        return y;
    }
    

一方、サブタイピングベースのインターフェイスでは、異種コレクションの形成が自然に可能になります。たとえば、型のリストにList<C>は、さまざまなサブタイプを持つメンバーを含めることができますC(ただし、ダウンキャストを使用する場合を除き、正確な型を復元することはできません)。型クラスに基づいて同じことを行うには、追加機能として存在型が必要です。


ああ、それはとても理にかなっています。タイプ対値ベースのディスパッチは、おそらく適切に考えていなかった大きなことです。パラメトリック多相性とより具体的なタイピングの問題は理にかなっています。私はちょうどそれとサブタイプベースのインターフェースを一緒に思い描いていました(明らかにJavaのようです:-/)。
oconnor0

存在型Cは、ダウンキャストが存在しないサブタイプの作成に似ていますか?
oconnor0

やや。それらは、型を抽象化する、つまりその表現を隠す手段です。Haskellでは、クラス制約も追加する場合、そのクラスのメソッドを使用できますが、それ以外は使用できません。-ダウンキャストは、実際にはサブタイピングと実存的定量化の両方とは別の機能であり、原則として、後者の存在下で追加することもできます。それを提供しないオブジェクト指向言語があるように。
アンドレアスロスバーグ

PS:FWIW、Javaのワイルドカード型は実存型ですが、かなり限定的でアドホックです(これは、やや混乱を招く理由の一部かもしれません)。
アンドレアスロスバーグ

1
@didierc、これは静的に完全に解決できるケースに制限されます。さらに、型クラスを一致させるには、戻り型のみに基づいて区別できるオーバーロード解決の形式が必要です(項目1を参照)。
アンドレアスロスバーグ

6

アンドレアスの優れた答えに加えて、型クラスはグローバルな名前空間に影響を与えるオーバーロードを合理化することを意図していることに留意してください。Haskellには、型クラスを介して取得できるもの以外にオーバーロードはありません。対照的に、オブジェクトインターフェイスを使用する場合、そのインターフェイスの引数を取るように宣言されている関数のみが、そのインターフェイスの関数名を考慮する必要があります。したがって、インターフェイスはローカルネームスペースを提供します。

たとえば、fmap「Functor」というオブジェクトインターフェイスがありました。別のものを持っていることは完全にOKになりfmap、別のインターフェイスで「Structorを」と言います。各オブジェクト(またはクラス)は、実装するインターフェイスを選択できます。対照的に、Haskellではfmap、特定のコンテキスト内に1つしか持てません。Functor型クラスとStructor型クラスの両方を同じコンテキストにインポートすることはできません。

オブジェクトインターフェイスは、型クラスよりも標準ML署名に似ています。


それでも、MLモジュールとHaskell型クラスの間には密接な関係があるようです。cse.unsw.edu.au/~chak/papers/DHC07.html
スティーブンショー

1

具体的な例(Functor型クラスを使用)では、HaskellとJavaの実装の動作が異なります。多分データ型があり、それをFunctorにしたいことを想像してください(Haskellで本当に人気のあるデータ型で、Javaでも簡単に実装できます)。Javaの例では、MaybeクラスにFunctorインターフェースを実装させます。だから、次のように書くことができます(私はC#の背景しか持っていないので、単なる擬似コード):

Maybe<Int> val = new Maybe<Int>(5);
Functor<Int> res = val.fmap(someFunctionHere);

res多分ではなく、タイプがFunctorであることに注意してください。したがって、具体的な型情報を失い、キャストを行う必要があるため、Java実装はほとんど使用できなくなります。(少なくとも、型がまだ存在するような実装を作成できませんでした)。Haskell型クラスを使用すると、結果としてMaybe Intを取得できます。


この問題は、Javaがより高い種類の型をサポートしていないためであり、インターフェイスと型クラスの議論に関連していないと思います。Javaの種類が高ければ、fmapはaを非常にうまく返すことができMaybe<Int>ます。
dcastro
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.