web-dev-qa-db-ja.com

Haskellでの動的ディスパッチ

たとえば、Javaで記述されたプログラムは、動的ディスパッチに大きく依存しています。

そのようなプログラムはHaskellのような関数型言語でどのように表現されますか?

言い換えれば、「動的ディスパッチ」の下でアイデアを表現するハスケルの方法は何ですか?

50
user782220

答えは一見単純です:高階関数。 OO言語の仮想メソッドを持つオブジェクトは、いくつかのローカル状態と一緒に関数の栄光の記録にすぎません。Haskellでは、関数のレコードを直接使用して、ローカル状態をそれらに格納できます。閉鎖。

より具体的には、OOオブジェクトは次のもので構成されます。

  • オブジェクトのクラスの仮想メソッドの実装を含むvtable(仮想メソッドテーブル)へのポインター(vptr)。言い換えれば、関数ポインタの束。機能の記録。特に、各関数には、オブジェクト自体である非表示のパラメーターがあり、暗黙的に渡されます。
  • オブジェクトのデータメンバー(ローカル状態)

多くの場合、オブジェクトと仮想関数の建物全体は、クロージャーのサポートがないための手の込んだ回避策のように感じます。

たとえば、JavaのComparatorインターフェイスについて考えてみます。

_public interface Comparator<T> {
    int compare(T o1, T o2); // virtual (per default)
}
_

また、これを使用して、文字列のN番目の文字に基づいて文字列のリストを並べ替えるとします(十分な長さがあると想定します)。クラスを定義します。

_public class MyComparator implements Comparator<String> {
    private final int _n;

    MyComparator(int n) {
        _n = n;
    }

    int compare(String s1, String s2) {
        return s1.charAt(_n) - s2.charAt(_n);
    }
}
_

そして、あなたはそれを使用します:

_Collections.sort(myList, new MyComparator(5));      
_

Haskellでは次のようにします:

_sortBy :: (a -> a -> Ordering) -> [a] -> [a]

myComparator :: Int -> (String -> String -> Ordering)
myComparator n = \s1 s2 -> (s1 !! n) `compare` (s2 !! n)
-- n is implicitly stored in the closure of the function we return

foo = sortBy (myComparator 5) myList
_

Haskellに慣れていない場合は、ある種の疑似Javaで大まかにどのように見えるかを次に示します(これは1回だけ行います)。

_public void <T> sortBy(List<T> list, Ordering FUNCTION(T, T) comparator) { ... }

public (Ordering FUNCTION(String, String)) myComparator(int n) {
    return FUNCTION(String s1, String s2) {
        return s1[n].compare(s2[n]);
    }
}

public void foo() {
    sortBy(myList, myComparator(5));
}
_

タイプを定義していないことに注意してください。使用したのは関数だけです。どちらの場合も、sort関数に渡した「ペイロード」は、2つの要素を受け取り、それらの相対的な順序を与える関数でした。あるケースでは、これは、インターフェースを実装するタイプを定義し、その仮想関数を適切な方法で実装し、そのタイプのオブジェクトを渡すことによって達成されました。それ以外の場合は、関数を直接渡しました。どちらの場合も、sort関数に渡したものに内部整数を格納しました。 1つのケースでは、これはプライベートデータメンバーを型に追加することによって行われ、もう1つのケースでは、関数でそれを参照するだけで、関数のクロージャーに保持されます。

イベントハンドラーを備えたウィジェットのより複雑な例を考えてみましょう。

_public class Widget {
    public void onMouseClick(int x, int y) { }
    public void onKeyPress(Key key) { }
    public void Paint() { }
    ...
}

public class MyWidget extends Widget {
    private Foo _foo;
    private Bar _bar;
    MyWidget(...) {
        _foo = something;
        _bar = something; 
    }
    public void onMouseClick(int x, int y) {
        ...do stuff with _foo and _bar...
    }
}
_

Haskellでは次のように行うことができます:

_data Widget = Widget {
    onMouseClick :: Int -> Int -> IO (),
    onKeyPress   :: Key -> IO (),
    Paint        :: IO (),
    ...
}

constructMyWidget :: ... -> IO Widget
constructMyWidget = do
    foo <- newIORef someFoo
    bar <- newIORef someBar
    return $ Widget {
        onMouseClick = \x y -> do
            ... do stuff with foo and bar ...,
        onKeyPress = \key -> do ...,
        Paint = do ...
    }
_

最初のWidgetの後、タイプを定義しなかったことに再度注意してください。関数のレコードを作成し、クロージャに格納する関数のみを記述しました。これは、ほとんどの場合、OO言語でサブクラスを定義する唯一の理由でもあります。前の例との唯一の違いは、1つの関数の代わりに複数の関数があることです。 Java caseは、インターフェイス(およびその実装)に複数の関数を配置するだけでエンコードされ、Haskellでは単一の関数ではなく関数のレコードを渡すことでエンコードされます(レコードを渡すこともできます)。前の例では単一の関数が含まれていましたが、そのようには感じませんでした。)

(多くの場合、動的ディスパッチは必要ありません動的ディスパッチは必要ありません。タイプのデフォルトの順序に基づいてリストを並べ替えるだけの場合は、どこかで注意する価値があります。静的に選択された特定のOrdタイプに対して定義されたaインスタンスを使用する_sort :: Ord a => [a] -> [a]_を使用するだけです。)

タイプベースの動的ディスパッチ

上記のJavaアプローチとHaskellアプローチの違いの1つは、Javaアプローチでは、オブジェクトの動作(ローカル状態を除く)が決定されることです。そのタイプによって(またはそれほど慈善的ではありませんが、各実装には新しいタイプが必要です)Haskellでは、関数のレコードを好きなように作成しています。ほとんどの場合、これは純粋な勝利です(柔軟性が得られ、何も失われません)が、何らかの理由でそれが必要なのはJava方法です。その場合、他の回答で述べられているように、進む方法は型クラスと存在です。

Widgetの例を続けるために、Widgetの実装をその型から追跡したいとします(実装ごとに新しい型を必要とします)。型クラスを定義して、型をその実装にマップします。

_-- the same record as before, we just gave it a different name
data WidgetImpl = WidgetImpl {
    onMouseClick :: Int -> Int -> IO (),
    onKeyPress   :: Key -> IO (),
    Paint        :: IO (),
    ...
}

class IsWidget a where
    widgetImpl :: a -> WidgetImpl

data Widget = forall a. IsWidget a => Widget a

sendClick :: Int -> Int -> Widget -> IO ()
sendClick x y (Widget a) = onMouseClick (widgetImpl a) x y

data MyWidget = MyWidget {
    foo :: IORef Foo,
    bar :: IORef Bar
}

constructMyWidget :: ... -> IO MyWidget
constructMyWidget = do
    foo_ <- newIORef someFoo
    bar_ <- newIORef someBar
    return $ MyWidget {
        foo = foo_,
        bar = bar_
    }

instance IsWidget MyWidget where
    widgetImpl myWidget = WidgetImpl {
            onMouseClick = \x y -> do
                ... do stuff with (foo myWidget) and (bar myWidget) ...,
            onKeyPress = \key -> do ...,
            Paint = do ...
        }
_

関数のレコードを取得するためだけのクラスがあり、それを個別に関数から取り出さなければならないのは少し厄介です。私はこの方法で型クラスの個別の側面を説明するだけでした。それらは関数の栄光の記録(以下で使用)と、コンパイラが推測された型(上記で使用)に基づいて適切なレコードを挿入する魔法です。 、および以下を使用し続けます)。単純化しましょう:

_class IsWidget a where
    onMouseClick :: Int -> Int -> a -> IO ()
    onKeyPress   :: Key -> a -> IO ()
    Paint        :: a -> IO ()
    ...

instance IsWidget MyWidget where
    onMouseClick x y myWidget = ... do stuff with (foo myWidget) and (bar myWidget) ...
    onKeyPress key myWidget = ...
    Paint myWidget = ...

sendClick :: Int -> Int -> Widget -> IO ()
sendClick x y (Widget a) = onMouseClick x y a

-- the rest is unchanged from above
_

このスタイルは、OO言語から来た人々によく採用されます。これは、OO言語が行う方法から、より馴染みがあり、1対1のマッピングに近いためです。しかし、ほとんどの目的では、最初のセクションで説明したアプローチよりも複雑で柔軟性がありません。その理由は、さまざまなウィジェットの重要な点がウィジェット機能の実装方法だけである場合、作成する意味がほとんどないためです。型、それらの型のインターフェースのインスタンス、そしてそれらを既存のラッパーに入れることによって、基礎となる型を再び抽象化します。関数を直接渡す方が簡単です。

私ができる考えられる利点の1つは、Haskellにはサブタイプがないが、「サブクラス化」(おそらくサブインターフェースまたはサブ制約と呼ばれる方がよい)があることです。たとえば、次のことができます。

_class IsWidget a => IsWidgetExtra a where
    ...additional methods to implement...
_

そして、IsWidgetExtraがある任意のタイプで、IsWidgetのメソッドをシームレスに使用することもできます。レコードベースのアプローチの唯一の代替方法は、レコード内にレコードを作成することです。これには、内部レコードの手動によるラップとアンラップが含まれます。しかし、これは、OO言語の深いクラス階層を明示的にエミュレートしたい場合にのみ有利です。これは、自分の生活を困難にしたい場合にのみ行います(注も参照)。 Haskellには、IsWidgetからIsWidgetExtraに動的にダウンキャストする組み込みの方法はありません。しかし、 ifcxt )があります。

(レコードベースのアプローチの利点はどうですか?新しいことをするたびに新しい型を定義する必要がないことに加えて、レコードは単純な値レベルのものであり、値は型よりもはるかに簡単に操作できます。たとえば、引数としてWidgetを取り、それに基づいて新しいWidgetを作成する関数を記述します。いくつかは異なり、他は同じままです。これは、からのサブクラス化のようなものです。 C++のテンプレートパラメータで、混乱が少なくなります。)

用語集

  • 高階関数:他の関数を引数として取る(または結果として返す)関数

  • レコード:struct(パブリックデータメンバーのみを含むクラス)。辞書とも呼ばれます。

  • クロージャ:関数型言語(および他の多くの言語)を使用すると、定義サイトのスコープ内のもの(たとえば、外部関数の引数)を参照するローカル関数(関数内の関数、ラムダ)を定義できます。維持されますが、関数の「クロージャ」にあります。または、plusのように2つのintを取り、intを返す関数がある場合は、それを1つの引数、たとえば_5_に適用すると、結果はintを受け取る関数になります。そして、それに5を追加することにより、intを返します。その場合、_5_も結果の関数のクロージャーに格納されます。 (他のコンテキストでは、「クロージャ」は「クロージャのある関数」を意味するために使用されることもあります。)

  • 型クラス: not OO言語のクラスと同じ。インターフェースのようなものですが、非常に異なります。 を参照)ここ

編集29-11-14:この答えの核心はまだ本質的に正しいと思いますが(HaskellのHOFはOOPの仮想メソッドに対応します)、私がそれを書いたときから私の価値判断は微妙になりました。特に、HaskellのアプローチもOOPのアプローチも、他のアプローチよりも厳密に「より基本的」ではないと今では思います。 このredditコメント を参照してください。

67
glaebhoerl

実際にdynamicディスパッチを必要とせず、ポリモーフィズムだけを必要とする頻度は驚くべきものです。

たとえば、リスト内のすべてのデータを並べ替える関数を作成する場合は、それをポリモーフィックにする必要があります。 (つまり、notは、すべてのタイプに対してこの関数を手動で再実装する必要はありません。それは悪いことです。)しかし、そうではありません。実際には何かが必要ですdynamic;コンパイル時に、ソートする1​​つまたは複数のリストに実際に何が含まれているかがわかります。したがって、この場合、実際にはrun-timeタイプルックアップはまったく必要ありません。

Haskellでは、物を移動したいだけで、それがどのタイプであるかを知る必要も気にする必要もない場合は、いわゆる「パラメトリックポリモーフィズム」を使用できます。これはおおよそJavaジェネリックのようなものです。またはC++テンプレート。データに関数を適用できるようにする必要がある場合(たとえば、順序の比較が必要なデータを並べ替える場合)、引数としてそれを行う関数を渡すことができます。あるいは、HaskellにはJavaインターフェースに少し似たものがあり、「このソート関数は、このインターフェースを実装するあらゆるタイプのデータを受け入れる」と言うことができます。

これまでのところ、dynamicディスパッチはまったくなく、静的のみです。関数を引数として渡すことができるため、手動で「ディスパッチ」を実行できることにも注意してください。

本当に必要実際の動的ディスパッチする場合は、「既存のタイプ」を使用できます。 "、またはData.Dynamicライブラリや同様のトリックを使用できます。

10

アドホック多相typeclasses を介して行われます。より多くのOOPのようなDDは 既存のタイプ でエミュレートされます。

7
Cfr

たぶんあなたはADTとパターンマッチングが必要ですか?

data Animal = Dog {dogName :: String}
            | Cat {catName :: String}
            | Unicorn

say :: Animal -> String
say (Dog {dogName = name}) = "Woof Woof, my name is " ++ name
say (Cat {catName = name}) = "Meow meow, my name is " ++ name
say Unicorn = "Unicorns do not talk"
5
s9gf4ult