Ich denke tatsächlich, dass Rückgabetyp-Polymorphismus eines der besten Merkmale von Typklassen ist. Nachdem ich es eine Weile benutzt habe, fällt es mir manchmal schwer, zum OOP-Modell zurückzukehren, wo ich es nicht habe.
Betrachten Sie die Kodierung der Algebra. In Haskell haben wir eine Typenklasse Monoid
(ignorieren mconcat
)
class Monoid a where
mempty :: a
mappend :: a -> a -> a
Wie können wir dies als Schnittstelle in einer OO-Sprache codieren? Die kurze Antwort lautet: Wir können nicht. Das ist , weil die Art der mempty
ist (Monoid a) => a
auch bekannt als Rückgabetyp Polymorphismus. Die Fähigkeit, Algebra zu modellieren, ist unglaublich nützlich, IMO. *
Sie beginnen Ihren Beitrag mit der Beschwerde über "Referentielle Transparenz". Dies wirft einen wichtigen Punkt auf: Haskell ist eine wertorientierte Sprache. Ausdrücke wie read 3
müssen also nicht als Dinge verstanden werden, die Werte berechnen, sondern können auch als Werte verstanden werden. Dies bedeutet, dass das eigentliche Problem nicht der Rückgabetyp-Polymorphismus ist: Es handelt sich um Werte mit polymorphem Typ ( []
und Nothing
). Wenn die Sprache diese haben soll, muss sie aus Konsistenzgründen wirklich polymorphe Rückgabetypen haben.
Sollte man sagen können, dass []
es sich um einen Typ handelt forall a. [a]
? Ich glaube schon. Diese Funktionen sind sehr nützlich und vereinfachen die Sprache erheblich.
Wenn Haskell einen Subtyp []
hätte, könnte Polymorphismus ein Subtyp für alle sein [a]
. Das Problem ist, dass ich keine Möglichkeit kenne, diese zu codieren, ohne dass der Typ der leeren Liste polymorph ist. Überlegen Sie, wie es in Scala gemacht wird (es ist kürzer als in der kanonischen statisch getippten OOP-Sprache Java)
abstract class List[A]
case class Nil[A] extends List[A]
case class Cons[A](h: A. t: List[A]) extends List[A]
Auch hier Nil()
ist ein Objekt vom Typ Nil[A]
**
Ein weiterer Vorteil des Rückkehrtyp-Polymorphismus besteht darin, dass das Einbetten von Curry-Howard viel einfacher wird.
Betrachten Sie die folgenden logischen Sätze:
t1 = forall P. forall Q. P -> P or Q
t2 = forall P. forall Q. P -> Q or P
Wir können diese Sätze in Haskell trivial erfassen:
data Either a b = Left a | Right b
t1 :: a -> Either a b
t1 = Left
t2 :: a -> Either b a
t2 = Right
Fazit: Ich mag den Rückgabetyp-Polymorphismus und denke nur, dass er die referenzielle Transparenz bricht, wenn Sie eine begrenzte Vorstellung von Werten haben (obwohl dies im Fall von Ad-hoc-Typklassen weniger überzeugend ist). Auf der anderen Seite finde ich Ihre Argumente zu MR und tippe zwingend.
*. In den Kommentaren weist ysdx darauf hin, dass dies nicht strikt zutrifft: Wir könnten Typklassen erneut implementieren, indem wir die Algebra als einen anderen Typ modellieren. Wie die Java:
abstract class Monoid<M>{
abstract M empty();
abstract M append(M m1, M m2);
}
Sie müssen dann Objekte dieses Typs mit sich herumreichen. Scala kennt implizite Parameter, mit denen einige, aber meiner Erfahrung nach nicht alle der Mehraufwand für die explizite Verwaltung dieser Dinge vermieden werden. Das Einfügen Ihrer Dienstprogrammmethoden (Factory-Methoden, Binärmethoden usw.) in einen separaten F-Typ erweist sich als äußerst nützliche Methode zum Verwalten von Dingen in einer OO-Sprache, die Generika unterstützt. Trotzdem bin ich mir nicht sicher, ob ich dieses Muster hätte anwenden können, wenn ich keine Erfahrung darin gehabt hätte, Dinge mit Typenklassen zu modellieren, und ich bin mir nicht sicher, ob andere das auch tun.
Es gibt auch Einschränkungen, da es nicht möglich ist, ein Objekt zu erhalten, das die Typenklasse für einen beliebigen Typ implementiert. Sie müssen entweder die Werte explizit übergeben, so etwas wie die Implikate von Scala verwenden oder eine Form der Abhängigkeitsinjektionstechnologie verwenden. Das Leben wird hässlich. Auf der anderen Seite ist es schön, dass Sie mehrere Implementierungen für den gleichen Typ haben können. Etwas kann in mehrfacher Hinsicht ein Monoid sein. Wenn man diese Strukturen separat herumträgt, fühlt sich IMO mathematisch moderner und konstruktiver an. Also, obwohl ich immer noch die Haskell-Methode bevorzuge, habe ich meinen Fall wahrscheinlich übertrieben.
Typenklassen mit Rückkehrtyp-Polymorphismus machen diese Art von Dingen einfach zu handhaben. Das heißt nicht, es ist der beste Weg, es zu tun.
**. Jörg W Mittag weist darauf hin, dass dies in der Scala nicht wirklich kanonisch ist. Stattdessen würden wir der Standardbibliothek etwas ähnlicheres folgen:
abstract class List[+A] ...
case class Cons[A](head: A, tail: List[A]) extends List[A] ...
case object Nil extends List[Nothing] ...
Dies nutzt die Unterstützung von Scala für Bottom-Typen sowie für kovariante Typparameter. Also Nil
ist vom Typ Nil
nicht Nil[A]
. Zu diesem Zeitpunkt sind wir ziemlich weit von Haskell entfernt, aber es ist interessant festzustellen, wie Haskell den unteren Typ darstellt
undefined :: forall a. a
Das heißt, es ist nicht der Subtyp aller Typen, sondern polymorph (sp) ein Mitglied aller Typen.
Noch mehr Rückkehrtyp-Polymorphismus.