Ja, eine sehr einfache Frage an der Oberfläche. Aber wenn Sie sich die Zeit nehmen, es bis zum Ende durchzudenken, gelangen Sie in die unermesslichen Tiefen der Typentheorie. Und die Typentheorie starrt auch in dich hinein.
Zunächst haben Sie natürlich bereits richtig herausgefunden, dass F # keine Typklassen hat, und deshalb. Sie schlagen jedoch eine Schnittstelle vor Mappable
. Ok, schauen wir uns das an.
Nehmen wir an, wir können eine solche Schnittstelle deklarieren. Können Sie sich vorstellen, wie die Signatur aussehen würde?
type Mappable =
abstract member map : ('a -> 'b) -> 'f<'a> -> 'f<'b>
Wo f
ist der Typ, der die Schnittstelle implementiert? Oh, Moment mal! F # hat das auch nicht! Hier f
ist eine höherwertige Typvariable, und F # hat überhaupt keine höherwertige. Es gibt keine Möglichkeit, eine Funktion f : 'm<'a> -> 'm<'b>
oder ähnliches zu deklarieren .
Aber ok, sagen wir, wir haben auch diese Hürde überwunden. Und jetzt haben wir eine Schnittstelle , Mappable
die von implementiert werden kann List
, Array
, Seq
, und die Küchenspüle. Aber warte! Jetzt haben wir eine Methode anstelle einer Funktion, und Methoden lassen sich nicht gut zusammensetzen! Schauen wir uns an, wie Sie jedem Element einer verschachtelten Liste 42 hinzufügen:
// Good ol' functions:
add42 nestedList = nestedList |> List.map (List.map ((+) 42))
// Using an interface:
add42 nestedList = nestedList.map (fun l -> l.map ((+) 42))
Schauen Sie: Jetzt müssen wir einen Lambda-Ausdruck verwenden! Es gibt keine Möglichkeit, diese .map
Implementierung als Wert an eine andere Funktion zu übergeben. Tatsächlich funktioniert das Ende von "Funktionen als Werte" (und ja, ich weiß, die Verwendung eines Lambda sieht in diesem Beispiel nicht sehr schlecht aus, aber glauben Sie mir, es wird sehr hässlich)
Aber warte, wir sind immer noch nicht fertig. Jetzt, da es sich um einen Methodenaufruf handelt, funktioniert die Typinferenz nicht mehr! Da die Typensignatur einer .NET-Methode vom Typ des Objekts abhängt, kann der Compiler nicht auf beide schließen. Dies ist tatsächlich ein sehr häufiges Problem, auf das Neulinge bei der Zusammenarbeit mit .NET-Bibliotheken stoßen. Und die einzige Heilung besteht darin, eine Typensignatur bereitzustellen:
add42 (nestedList : #Mappable) = nestedList.map (fun l -> l.map ((+) 42))
Oh, aber das reicht immer noch nicht! Obwohl ich eine Signatur für sich nestedList
selbst bereitgestellt habe , habe ich keine Signatur für den Lambda-Parameter bereitgestellt l
. Was soll eine solche Unterschrift sein? Würden Sie sagen, dass es sein sollte fun (l: #Mappable) -> ...
? Oh, und jetzt haben wir endlich Rang-N-Typen, wie Sie sehen, #Mappable
ist eine Abkürzung für "jeden Typ 'a
wie diesen 'a :> Mappable
" - dh einen Lambda-Ausdruck, der selbst generisch ist.
Oder alternativ könnten wir zur höheren Güte zurückkehren und den Typ nestedList
genauer erklären :
add42 (nestedList : 'f<'a<'b>> where 'f :> Mappable, 'a :> Mappable) = ...
Aber ok, lassen Sie uns die Typinferenz vorerst beiseite legen und zum Lambda-Ausdruck zurückkehren und wie wir jetzt nicht map
als Wert an eine andere Funktion übergeben können. Nehmen wir an, wir erweitern die Syntax ein wenig, um etwa das zu ermöglichen, was Elm mit Datensatzfeldern macht:
add42 nestedList = nestedList.map (.map ((+) 42))
Was wäre die Art von .map
? Es müsste ein eingeschränkter Typ sein, genau wie in Haskell!
.map : Mappable 'f => ('a -> 'b) -> 'f<'a> -> 'f<'b>
Wow OK. Abgesehen von der Tatsache, dass .NET solche Typen nicht einmal zulässt, haben wir effektiv gerade Typklassen zurückbekommen!
Es gibt jedoch einen Grund, warum F # überhaupt keine Typklassen hat. Viele Aspekte dieses Grundes sind oben beschrieben, aber eine präzisere Art, es auszudrücken, ist: Einfachheit .
Wie Sie sehen, ist dies ein Wollknäuel. Sobald Sie Typklassen haben, müssen Sie Einschränkungen, höhere Güte, Rang N (oder mindestens Rang 2) haben, und bevor Sie es wissen, fragen Sie nach aussagekräftigen Typen, Typfunktionen, GADTs und allem Rest davon.
Aber Haskell zahlt einen Preis für alle Leckereien. Es stellt sich heraus, dass es keinen guten Weg gibt, auf all das zu schließen . Höher sortierte Typen funktionieren irgendwie, Einschränkungen jedoch bereits nicht. Rang N - träume nicht einmal davon. Und selbst wenn es funktioniert, erhalten Sie Tippfehler, für deren Verständnis Sie einen Doktortitel benötigen. Und deshalb werden Sie in Haskell sanft ermutigt , auf alles Typensignaturen zu setzen. Nun, nicht alles - alles , aber wirklich fast alles. Und wo Sie keine Typensignaturen einfügen (z. B. innen let
und where
) - Überraschung-Überraschung, diese Orte sind tatsächlich monomorphisiert, sodass Sie im Wesentlichen wieder im simplen F # -Land sind.
In F # hingegen sind Typensignaturen selten, meist nur zur Dokumentation oder für .NET Interop. Außerhalb dieser beiden Fälle können Sie ein ganzes großes komplexes Programm in F # schreiben und keine Typensignatur einmal verwenden. Die Typinferenz funktioniert einwandfrei, da nichts zu komplex oder mehrdeutig ist, als dass sie verarbeitet werden könnte.
Und das ist der große Vorteil von F # gegenüber Haskell. Ja, mit Haskell können Sie sehr komplexe Dinge sehr präzise ausdrücken, das ist gut. Aber mit F # können Sie sehr verwaschen sein, fast wie Python oder Ruby, und der Compiler kann Sie trotzdem fangen, wenn Sie stolpern.