Warum gibt es in F # so viele Kartenfunktionen für verschiedene Typen?


9

Ich lerne F #. Ich habe FP mit Haskell gestartet und bin neugierig geworden.

Da F # eine .NET-Sprache ist, scheint es für mich vernünftiger zu sein, eine Schnittstelle Mappablewie die haskell- FunctorTypklasse zu deklarieren .

Geben Sie hier die Bildbeschreibung ein

Aber wie im obigen Bild werden F # -Funktionen getrennt und eigenständig implementiert. Was ist der Designzweck eines solchen Designs? Für mich Mappable.mapwäre es bequemer, dies für jeden Datentyp einzuführen und zu implementieren.


Diese Frage gehört nicht zu SO. Es ist kein Programmierproblem. Ich schlage vor, Sie fragen in F # Slack oder einem anderen Diskussionsforum.
Bent Tranberg

5
@BentTranberg Großzügig gelesen, The community is here to help you with specific coding, algorithm, or language problems.würde auch Fragen zum Sprachdesign umfassen, solange die anderen Kriterien erfüllt sind.
kaefer

3
Kurz gesagt, F # hat keine Typklassen und muss daher mapfür jeden Sammlungstyp neu implementiert und andere allgemeine Funktionen höherer Ordnung verwendet werden. Eine Schnittstelle würde wenig helfen, da immer noch jeder Sammlungstyp eine separate Implementierung bereitstellen muss.
Dumetrulo

Antworten:


19

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 fist der Typ, der die Schnittstelle implementiert? Oh, Moment mal! F # hat das auch nicht! Hier fist 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 , Mappabledie 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 .mapImplementierung 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 nestedListselbst 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, #Mappableist eine Abkürzung für "jeden Typ 'awie 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 nestedListgenauer 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 mapals 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 letund 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.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.