Ich mache seit einer Weile Entwickler in F # und es gefällt mir. Ein Schlagwort, von dem ich weiß, dass es in F # nicht existiert, sind höherwertige Typen. Ich habe Material über höherwertige Typen gelesen und glaube, ich verstehe deren Definition. Ich bin mir nur nicht sicher, warum sie nützlich sind. Kann jemand einige Beispiele dafür nennen, was höherwertige Typen in Scala oder Haskell einfach machen, die Problemumgehungen in F # erfordern? Was wären die Problemumgehungen auch für diese Beispiele ohne höherwertige Typen (oder umgekehrt in F #)? Vielleicht bin ich es einfach so gewohnt, es zu umgehen, dass ich das Fehlen dieser Funktion nicht bemerke.
(Ich denke) Ich verstehe, dass Sie anstelle von myList |> List.map f
oder myList |> Seq.map f |> Seq.toList
höherwertigen Typen einfach schreiben können myList |> map f
und es wird a zurückgegeben List
. Das ist großartig (vorausgesetzt es ist richtig), scheint aber irgendwie kleinlich zu sein? (Und könnte es nicht einfach durch Zulassen einer Funktionsüberladung geschehen?) Normalerweise konvertiere ich Seq
trotzdem und kann danach in alles konvertieren, was ich will. Vielleicht bin ich es einfach zu gewohnt, daran zu arbeiten. Aber gibt es ein Beispiel, bei dem höherwertige Typen Sie entweder bei Tastenanschlägen oder bei der Typensicherheit wirklich retten?
IMonad<T>
und sie dann zurücksetzen , z. B.IEnumerable<int>
oderIObservable<int>
wenn du fertig bist? Ist das alles nur, um Casting zu vermeiden?return
dies funktionieren würde, da dies wirklich zum Monadentyp gehört und nicht zu einer bestimmten Instanz, sodass Sie es überhaupt nicht in dieIMonad
Benutzeroberfläche einfügen möchten .bind
akaSelectMany
etc werfen . Was bedeutet, dass jemand die API zubind
einemIObservable
zu einem verwenden könnteIEnumerable
und davon ausgehen könnte, dass es funktionieren würde. Ja, wenn das der Fall ist und daran führt kein Weg vorbei. Nur nicht 100% sicher, dass es keinen Weg daran vorbei gibt.Antworten:
Die Art eines Typs ist also sein einfacher Typ. Zum Beispiel
Int
hat kind,*
was bedeutet, dass es ein Basistyp ist und durch Werte instanziiert werden kann. Nach einer losen Definition eines höherwertigen Typs (und ich bin nicht sicher, wo F # die Linie zeichnet, also lassen Sie es uns einfach einschließen) sind polymorphe Container ein großartiges Beispiel für einen höherwertigen Typ.Der Typkonstruktor
List
hat eine Art,* -> *
was bedeutet, dass ihm ein konkreter Typ übergeben werden muss, um einen konkreten Typ zu erhalten:List Int
kann Einwohner haben wie[1,2,3]
,List
kann sich aber nicht.Ich gehe davon aus, dass die Vorteile von polymorphen Behältern offensichtlich sind, aber es gibt nützlichere
* -> *
Arten als nur die Behälter. Zum Beispiel die Beziehungenoder Parser
beide haben auch nett
* -> *
.Wir können dies in Haskell jedoch weiterführen, indem wir Typen mit noch höherwertigen Arten haben. Zum Beispiel könnten wir einen Typ mit Art suchen
(* -> *) -> *
. Ein einfaches Beispiel hierfür könnte sein,Shape
dass versucht wird, einen Container der Art zu füllen* -> *
.Dies ist beispielsweise zur Charakterisierung von
Traversable
s in Haskell nützlich , da sie immer in Form und Inhalt unterteilt werden können.Als weiteres Beispiel betrachten wir einen Baum, der nach der Art des Zweigs parametrisiert ist. Zum Beispiel könnte ein normaler Baum sein
Aber wir können sehen, dass der Verzweigungstyp ein
Pair
vonTree a
s enthält, und so können wir dieses Stück parametrisch aus dem Typ extrahierenDieser
TreeG
Typkonstruktor hat kind(* -> *) -> * -> *
. Wir können es verwenden, um interessante andere Variationen wie a zu machenRoseTree
Oder pathologische wie a
MaybeTree
Oder ein
TreeTree
Ein weiterer Ort, an dem dies auftaucht, sind "Algebren von Funktoren". Wenn wir ein paar Schichten der Abstraktheit fallen lassen, könnte dies besser als eine Falte betrachtet werden, wie z
sum :: [Int] -> Int
. Algebren werden über den Funktor und den Träger parametrisiert . Der Funktor hat Art* -> *
und der Träger Art*
insgesamthat Art
(* -> *) -> * -> *
.Alg
nützlich wegen seiner Beziehung zu Datentypen und darauf aufgebauten Rekursionsschemata.Obwohl sie theoretisch möglich sind, habe ich noch nie einen Konstruktor mit noch höherwertigem Typ gesehen. Wir sehen manchmal Funktionen dieses Typs wie
mask :: ((forall a. IO a -> IO a) -> IO b) -> IO b
, aber ich denke, Sie müssen sich mit Typ-Prolog oder abhängig typisierter Literatur befassen, um diesen Grad an Komplexität in Typen zu erkennen.quelle
TreeTree
ist nur pathologisch, aber praktischer bedeutet es, dass Sie zwei verschiedene Arten von Bäumen miteinander verwoben haben. Wenn Sie diese Idee ein wenig weiter vorantreiben, erhalten Sie einige sehr mächtige typsichere Begriffe wie statisch sicheres Rot / schwarze Bäume und der gepflegte statisch ausgewogene FingerTree-Typ.Betrachten Sie die
Functor
Typklasse in Haskell, in derf
sich eine höherwertige Typvariable befindet:Was diese Art Unterschrift sagt , ist , dass fmap den Typ - Parameter eines ändert sich
f
vona
zub
, aber Blätterf
wie es war. Wenn Sie alsofmap
über eine Liste verwenden, erhalten Sie eine Liste, wenn Sie sie über einen Parser verwenden, erhalten Sie einen Parser und so weiter. Und dies sind statische Garantien zur Kompilierungszeit.Ich kenne F # nicht, aber lassen Sie uns überlegen, was passiert, wenn wir versuchen, die
Functor
Abstraktion in einer Sprache wie Java oder C # auszudrücken , mit Vererbung und Generika, aber ohne höherwertige Generika. Erster Versuch:Das Problem bei diesem ersten Versuch ist, dass eine Implementierung der Schnittstelle jede implementierte Klasse zurückgeben darf
Functor
. Jemand könnte eine schreiben,FunnyList<A> implements Functor<A>
derenmap
Methode eine andere Art von Sammlung zurückgibt, oder sogar etwas anderes, das überhaupt keine Sammlung ist, aber immer noch eineFunctor
. Wenn Sie diemap
Methode verwenden, können Sie für das Ergebnis keine subtypspezifischen Methoden aufrufen, es sei denn, Sie übertragen sie auf den Typ, den Sie tatsächlich erwarten. Wir haben also zwei Probleme:map
Methode immer dieselbeFunctor
Unterklasse wie der Empfänger zurückgibt .Functor
für das Ergebnis von aufzurufenmap
.Es gibt andere, kompliziertere Möglichkeiten, die Sie ausprobieren können, aber keine davon funktioniert wirklich. Sie können beispielsweise versuchen, den ersten Versuch zu erweitern, indem Sie Untertypen definieren
Functor
, die den Ergebnistyp einschränken:Dies hilft zu verhindern, dass Implementierer dieser schmaleren Schnittstellen den falschen Typ
Functor
von dermap
Methode zurückgeben. Da es jedoch keine Begrenzung für die Anzahl derFunctor
Implementierungen gibt, die Sie haben können, gibt es keine Begrenzung für die Anzahl der engeren Schnittstellen, die Sie benötigen.( BEARBEITEN: Und beachten Sie, dass dies nur funktioniert, weil es
Functor<B>
als Ergebnistyp angezeigt wird und die untergeordneten Schnittstellen es eingrenzen können. AFAIK Wir können also nicht beide VerwendungenMonad<B>
in der folgenden Schnittstelle eingrenzen:In Haskell mit höherrangigen Variablen ist dies
(>>=) :: Monad m => m a -> (a -> m b) -> m b
.)Ein weiterer Versuch besteht darin, rekursive Generika zu verwenden, um zu versuchen, dass die Schnittstelle den Ergebnistyp des Subtyps auf den Subtyp selbst beschränkt. Spielzeugbeispiel:
Aber diese Art von Technik (die für Ihren normalen OOP-Entwickler ziemlich geheimnisvoll ist, zum Teufel auch für Ihren normalen funktionalen Entwickler) kann die gewünschte
Functor
Einschränkung auch noch nicht ausdrücken :Das Problem hierbei ist, dass dies nicht darauf beschränkt ist
FB
, dasselbe zu habenF
wieFA
- so dassList<A> implements Functor<List<A>, A>
diemap
Methode beim Deklarieren eines Typs immer noch a zurückgeben kannNotAList<B> implements Functor<NotAList<B>, B>
.Letzter Versuch in Java unter Verwendung von Rohtypen (nicht parametrisierte Container):
Hier
F
werden nicht parametrisierte Typen wie justList
oder instanziiertMap
. Dies garantiert, dass aFunctorStrategy<List>
nur a zurückgeben kannList
- Sie haben jedoch die Verwendung von Typvariablen zum Verfolgen der Elementtypen der Listen aufgegeben.Das Herzstück des Problems ist, dass Sprachen wie Java und C # nicht zulassen, dass Typparameter Parameter haben. Wenn
T
es sich in Java um eine Typvariable handelt, können Sie schreibenT
undList<T>
, aber nichtT<String>
. Höher sortierte Typen heben diese Einschränkung auf, so dass Sie so etwas haben können (nicht vollständig durchdacht):Und insbesondere dieses Bit ansprechen:
Es gibt viele Sprachen, die die Idee der
map
Funktion auf diese Weise verallgemeinern , indem sie sie so modellieren, als ob es bei der Zuordnung im Kern um Sequenzen geht. Ihre Bemerkung ist in diesem Sinne: Wenn Sie einen Typ haben, der die Konvertierung von und nach unterstütztSeq
, erhalten Sie die Kartenoperation "kostenlos", indem Sie sie wiederverwendenSeq.map
.In Haskell ist die
Functor
Klasse jedoch allgemeiner; es ist nicht an den Begriff der Sequenzen gebunden. Sie könnenfmap
Typen implementieren , die keine gute Zuordnung zu Sequenzen aufweisen, z. B.IO
Aktionen, Parser-Kombinatoren, Funktionen usw.:Das Konzept des "Mappings" ist wirklich nicht an Sequenzen gebunden. Es ist am besten, die Funktorgesetze zu verstehen:
Sehr informell:
Aus diesem Grund möchten Sie
fmap
den Typ beibehalten. Sobald Siemap
Operationen erhalten, die einen anderen Ergebnistyp erzeugen, wird es sehr viel schwieriger, solche Garantien zu geben.quelle
fmap
auf ,Function a
wenn es bereits eine hat den.
Betrieb? Ich verstehe , warum.
macht die Definition des zu spüren ,fmap
op, aber ich verstehe es einfach nicht , wo Sie jemals verwenden müssen , würdefmap
statt.
. Wenn Sie ein Beispiel geben könnten, wo dies nützlich wäre, würde es mir vielleicht helfen, es zu verstehen.double
aus einem Funktor ein Fn machen , wo es ein Fndouble [1, 2, 3]
gibt[2, 4, 6]
unddouble sin
gibt, das doppelt so hoch ist wie die Sünde. Ich kann sehen, wo, wenn Sie anfangen, in dieser Denkweise zu denken, wenn Sie eine Karte auf einem Array ausführen, Sie ein Array zurück erwarten, nicht nur eine Sequenz, weil wir hier an Arrays arbeiten.Functor
und vom Client der Bibliothek ausgewählt werden kann. Die Antwort von J. Abrahamson liefert ein Beispiel: Rekursive Falten können mithilfe von Funktoren verallgemeinert werden. Ein anderes Beispiel sind freie Monaden; Sie können sich diese als eine Art generische Interpreter-Implementierungsbibliothek vorstellen, in der der Client den "Befehlssatz" als willkürlich bereitstelltFunctor
.Functor
oder a gegriffenSemiGroup
. Wo verwenden echte Programme diese Sprachfunktion am häufigsten?Ich möchte hier in einigen ausgezeichneten Antworten keine Informationen wiederholen, aber es gibt einen wichtigen Punkt, den ich hinzufügen möchte.
Normalerweise benötigen Sie keine höherwertigen Typen, um eine bestimmte Monade oder einen bestimmten Funktor (oder einen anwendbaren Funktor oder Pfeil oder ...) zu implementieren. Dabei fehlt jedoch meistens der Punkt.
Im Allgemeinen habe ich festgestellt, dass Menschen, die die Nützlichkeit von Funktoren / Monaden / Was auch immer nicht erkennen, oft daran denken, dass sie nacheinander an diese Dinge denken . Functor / monad / etc-Operationen fügen einer Instanz wirklich nichts hinzu (anstatt bind, fmap usw. aufzurufen, könnte ich einfach alle Operationen aufrufen, die ich zum Implementieren von bind, fmap usw. verwendet habe). Was Sie wirklich für diese Abstraktionen wollen, ist, dass Sie Code haben können, der generisch mit jedem Funktor / Monade / etc. Funktioniert.
In einem Kontext, in dem ein derartiger generischer Code weit verbreitet ist, bedeutet dies, dass Ihr Typ jedes Mal, wenn Sie eine neue Monadeninstanz schreiben, sofort Zugriff auf eine große Anzahl nützlicher Operationen erhält , die bereits für Sie geschrieben wurden . Das ist der Punkt , der Monaden zu sehen (und functors, und ...) überall; nicht so , dass ich verwenden kann ,
bind
anstattconcat
undmap
zu implementierenmyFunkyListOperation
(was mir nichts an mir gewinne), sondern vielmehr so , dass , wenn ich nach Bedarf kommenmyFunkyParserOperation
undmyFunkyIOOperation
ich kann ich den Code wiederverwenden ursprünglich Sägen in Bezug auf den Listen , weil es tatsächlich Monade-generic ist .Um jedoch über einen parametrisierten Typ wie eine Monade mit Typensicherheit hinweg zu abstrahieren , benötigen Sie höherwertige Typen (wie auch in anderen Antworten hier erläutert).
quelle
Für eine spezifischere .NET-spezifische Perspektive habe ich vor einiger Zeit einen Blog-Beitrag darüber geschrieben. Der springende Punkt dabei ist, dass Sie bei höherwertigen Typen möglicherweise dieselben LINQ-Blöcke zwischen
IEnumerables
undIObservables
wiederverwenden können. Ohne höherwertige Typen ist dies jedoch nicht möglich.Das Beste, was Sie bekommen können (ich habe es nach dem Posten des Blogs herausgefunden), ist, Ihr eigenes zu erstellen
IEnumerable<T>
undIObservable<T>
beide von einem zu erweiternIMonad<T>
. Dies würde es Ihnen ermöglichen, Ihre LINQ-Blöcke wiederzuverwenden, wenn sie gekennzeichnet sindIMonad<T>
, aber dann ist es nicht mehr typsicher, da Sie damit mischen und anpassen könnenIObservables
und sichIEnumerables
innerhalb desselben Blocks befinden, was zwar faszinierend klingt, um dies zu ermöglichen, Sie dies jedoch tun würden im Grunde nur ein undefiniertes Verhalten bekommen.Ich schrieb einen späteren Beitrag darüber, wie Haskell dies einfach macht. (Ein No-Op, wirklich - das Beschränken eines Blocks auf eine bestimmte Art von Monade erfordert Code; das Aktivieren der Wiederverwendung ist die Standardeinstellung).
quelle
IObservables
im Produktionscode verwendet habe.IObservable
, und Sie verwenden Ereignisse im Kapitel WinForms Ihres eigenen Buches.Das am häufigsten verwendete Beispiel für höherwertigen Polymorphismus in Haskell ist die
Monad
Schnittstelle.Functor
undApplicative
sind auf die gleiche Weise höher gesinnt, also werde ich zeigenFunctor
, um etwas Prägnantes zu zeigen.Untersuchen Sie nun diese Definition und sehen Sie sich an, wie die Typvariable
f
verwendet wird. Sie werden sehen, dassf
dies keinen Wert bedeuten kann. Sie können Werte in dieser Typensignatur identifizieren, da sie Argumente und Ergebnisse einer Funktion sind. Also die Typvariablena
undb
sind also Typen, die Werte haben können. So sind die Typausdrückef a
undf b
. Aber nichtf
selbst.f
ist ein Beispiel für eine höherwertige Typvariable. Angesichts dessen*
ist die Art von Typen, die Werte haben können,f
muss die Art haben* -> *
. Das heißt, es braucht einen Typ, der Werte haben kann, weil wir aus früheren Untersuchungen wissen, dassa
undb
Werte haben müssen. Und das wissen wir auchf a
undf b
muss Werte haben, daher wird ein Typ zurückgegeben, der Werte haben muss.Das macht die
f
in der DefinitionFunctor
verwendete Variable eines höherwertigen Typs.Das
Applicative
undMonad
Schnittstellen fügen mehr hinzu, aber sie sind kompatibel. Dies bedeutet, dass sie auch Typvariablen mit Art bearbeiten* -> *
.Die Arbeit an höherwertigen Typen führt zu einer zusätzlichen Abstraktionsebene - Sie müssen nicht nur Abstraktionen über Basistypen erstellen. Sie können auch Abstraktionen über Typen erstellen, die andere Typen ändern.
quelle