Typklassen vs Objektschnittstellen

33

Ich glaube nicht, dass ich Typenklassen verstehe. Ich habe irgendwo gelesen, dass es falsch und irreführend ist, sich Typklassen als "Interfaces" (von OO) vorzustellen, die ein Typ implementiert. Das Problem ist, ich habe ein Problem damit, sie als etwas anderes zu sehen und wie das falsch ist.

Wenn ich zum Beispiel eine Typklasse habe (in Haskell-Syntax)

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

Wie unterscheidet sich das von der Schnittstelle [1] (in Java-Syntax)

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

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

Ein möglicher Unterschied, den ich mir vorstellen kann, besteht darin, dass die Typklassenimplementierung, die bei einem bestimmten Aufruf verwendet wird, nicht spezifiziert, sondern aus der Umgebung bestimmt wird - beispielsweise durch Prüfen der verfügbaren Module für eine Implementierung für diesen Typ. Das scheint ein Implementierungsartefakt zu sein, das in einer OO-Sprache angesprochen werden könnte. wie der Compiler (oder die Laufzeit) nach einem Wrapper / Extender / Monkey-Patcher suchen könnte, der die erforderliche Schnittstelle für den Typ verfügbar macht.

Was vermisse ich?

[1] Beachten Sie, dass das f aArgument entfernt wurde, fmapda Sie diese Methode für ein Objekt aufrufen würden, da es sich um eine OO-Sprache handelt. Diese Schnittstelle setzt voraus, dass das f aArgument behoben wurde.

oconnor0
quelle

Antworten:

46

Typklassen ähneln in ihrer Grundform eher Objektschnittstellen. In vielerlei Hinsicht sind sie jedoch viel allgemeiner.

  1. Versand erfolgt nach Typen, nicht nach Werten. Für die Ausführung ist kein Wert erforderlich. Beispielsweise ist es möglich, den Ergebnistyp einer Funktion wie in der Haskell- ReadKlasse zu verteilen :

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

    Ein solcher Versand ist bei herkömmlichen OO eindeutig unmöglich.

  2. Typklassen erstrecken sich natürlich auf mehrere Versandarten, indem einfach mehrere Parameter angegeben werden:

    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. Instanzdefinitionen sind unabhängig von Klassen- und Typdefinitionen, wodurch sie modularer sind. Ein Typ T aus Modul A kann aus Modul M2 in eine Klasse C nachgerüstet werden, ohne die Definition von beiden zu ändern, indem einfach eine Instanz in Modul M3 bereitgestellt wird. In OO erfordert dies mehr esoterische (und weniger OO-ische) Sprachfunktionen wie Erweiterungsmethoden.

  4. Typklassen basieren auf parametrischem Polymorphismus, nicht auf Subtypisierung. Das ermöglicht eine genauere Eingabe. Betrachten Sie zB

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

    gegen

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

    Im ersten Fall hat das Anwenden pick '\0' 'x'einen Typ Char, im zweiten Fall wissen Sie nur, dass es sich um eine Aufzählung handelt. (Dies ist auch der Grund, warum die meisten OO-Sprachen heutzutage parametrischen Polymorphismus integrieren.)

  5. Eng verwandt ist die Frage der binären Methoden. Sie sind völlig natürlich mit Typklassen:

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

    Mit Subtyping allein ist die OrdSchnittstelle nicht auszudrücken. Sie benötigen eine kompliziertere, rekursivere Form oder einen parametrischen Polymorphismus mit der Bezeichnung "F-gebundene Quantifizierung", um dies genau zu tun. Vergleichen Sie Java Comparableund seine Verwendung:

    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;
    }
    

Auf der anderen Seite ermöglichen subtyping-basierte Interfaces natürlich die Bildung heterogener Sammlungen, z. B. kann eine Typenliste List<C>Mitglieder mit verschiedenen Subtypen enthalten C(obwohl es nicht möglich ist, ihren genauen Typ wiederherzustellen, außer durch Downcasts). Um dasselbe basierend auf Typklassen zu tun, benötigen Sie existenzielle Typen als zusätzliches Feature.

Andreas Rossberg
quelle
Ah, das macht sehr viel Sinn. Der Typ-gegen-Wert-Versand ist wahrscheinlich die große Sache, über die ich nicht richtig nachgedacht habe. Das Problem des parametrischen Polymorphismus und der spezifischeren Typisierung ist sinnvoll. Ich hatte gerade diese und Subtyping-basierte Schnittstellen in meinem Kopf zusammengezogen (anscheinend denke ich in Java: - /).
oconnor0
Sind existenzielle Typen so etwas wie die Schaffung von Untertypen Cohne die Anwesenheit von Unterdrückten?
oconnor0
So'ne Art. Sie sind ein Mittel, um einen Typ abstrakt zu machen, dh seine Repräsentation zu verbergen. Wenn Sie in Haskell auch Klasseneinschränkungen anhängen, können Sie weiterhin Methoden dieser Klassen verwenden, aber sonst nichts. - Downcasts sind eigentlich ein Merkmal, das sowohl von Subtypisierung als auch von existentieller Quantifizierung getrennt ist und grundsätzlich auch in Gegenwart von letzterer hinzugefügt werden könnte. So wie es OO-Sprachen gibt, die es nicht anbieten.
Andreas Rossberg
PS: FWIW, Platzhaltertypen in Java sind existenzielle Typen, wenn auch eher begrenzt und ad-hoc (was ein Teil des Grundes sein kann, warum sie etwas verwirrend sind).
Andreas Rossberg
1
@didierc, das wäre auf Fälle beschränkt, die statisch vollständig aufgelöst werden können. Um Typklassen abzugleichen, wäre außerdem eine Form der Überladungsauflösung erforderlich, die allein anhand des Rückgabetyps unterschieden werden kann (siehe Punkt 1).
Andreas Rossberg
6

Beachten Sie neben Andreas 'hervorragender Antwort auch, dass Typklassen das Überladen optimieren sollen , was sich auf den globalen Namensraum auswirkt. In Haskell gibt es keine andere Überladung als die, die Sie über Typklassen erhalten können. Wenn Sie dagegen Objektschnittstellen verwenden, müssen sich nur die Funktionen, für die die Argumente dieser Schnittstelle deklariert sind, um die Funktionsnamen in dieser Schnittstelle kümmern. Schnittstellen stellen also lokale Namensräume bereit.

Zum Beispiel hatten Sie fmapin einer Objektschnittstelle namens "Functor". Es wäre vollkommen in Ordnung, einen anderen fmapin einer anderen Schnittstelle zu haben, sagen wir "Structor". Jedes Objekt (oder jede Klasse) kann auswählen, welche Schnittstelle implementiert werden soll. Im Gegensatz dazu können Sie in Haskell nur einen fmapin einem bestimmten Kontext haben. Sie können nicht gleichzeitig die Typklassen Functor und Structor in denselben Kontext importieren.

Objektschnittstellen ähneln eher Standard-ML-Signaturen als Typklassen.

Uday Reddy
quelle
und doch scheint es eine enge Beziehung zwischen ML-Modulen und Haskell-Typklassen zu geben. cse.unsw.edu.au/~chak/papers/DHC07.html
Steven Shaw
1

In Ihrem konkreten Beispiel (mit Functor-Typklasse) verhalten sich Haskell- und Java-Implementierungen unterschiedlich. Stellen Sie sich vor, Sie haben den Datentyp "Vielleicht" und möchten, dass er Functor ist (ein in Haskell sehr beliebter Datentyp, den Sie auch in Java problemlos implementieren können). In Ihrem Java-Beispiel werden Sie die Klasse Maybe veranlassen, Ihre Functor-Schnittstelle zu implementieren. Sie können also Folgendes schreiben (nur Pseudocode, da ich nur C # -Hintergrund habe):

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

Beachten Sie, dass der resTyp Functor ist, nicht Vielleicht. Dies macht die Java-Implementierung nahezu unbrauchbar, da Sie konkrete Typinformationen verlieren und Casts durchführen müssen. (Zumindest konnte ich eine solche Implementierung nicht schreiben, bei der noch Typen vorhanden waren). Bei Haskell-Typklassen erhalten Sie als Ergebnis "Maybe Int".

struhtanov
quelle
Ich denke, dass dieses Problem darauf zurückzuführen ist, dass Java keine höherwertigen Typen unterstützt und nicht mit der Diskussion über Schnittstellen und Typklassen zusammenhängt. Wenn Java höhere Arten hätte, könnte fmap sehr gut a zurückgeben Maybe<Int>.
Dcastro