Ich habe gerade gelernt, wie man Curry macht, und obwohl ich denke, dass ich das Konzept verstehe, sehe ich keinen großen Vorteil darin, es zu benutzen.
Als einfaches Beispiel verwende ich eine Funktion, die zwei Werte addiert (geschrieben in ML). Die Version ohne Curry wäre
fun add(x, y) = x + y
und würde genannt werden als
add(3, 5)
während die Curry-Version ist
fun add x y = x + y
(* short for val add = fn x => fn y=> x + y *)
und würde genannt werden als
add 3 5
Es scheint mir nur syntaktischer Zucker zu sein, der einen Satz von Klammern aus der Definition und dem Aufruf der Funktion entfernt. Ich habe Curry als eines der wichtigsten Merkmale einer funktionalen Sprache gesehen und bin im Moment ein bisschen davon überwältigt. Das Konzept der Erstellung einer Funktionskette, die jeden einzelnen Parameter anstelle einer Tupelfunktion verwendet, scheint für eine einfache Änderung der Syntax recht kompliziert zu sein.
Ist die etwas einfachere Syntax die einzige Motivation zum Lernen, oder fehlen mir einige andere Vorteile, die in meinem sehr einfachen Beispiel nicht offensichtlich sind? Ist Curry nur syntaktischer Zucker?
quelle
Antworten:
Mit Curry-Funktionen können Sie abstraktere Funktionen einfacher wiederverwenden, da Sie sich spezialisieren können. Angenommen, Sie haben eine Additionsfunktion
und dass Sie jedem Mitglied einer Liste 2 hinzufügen möchten. In Haskell würden Sie dies tun:
Hier ist die Syntax leichter als wenn Sie eine Funktion erstellen müssten
add2
oder wenn Sie eine anonyme Lambda-Funktion machen mussten:
Sie können damit auch von verschiedenen Implementierungen abstrahieren. Angenommen, Sie hatten zwei Suchfunktionen. Eine aus einer Liste von Schlüssel / Wert-Paaren und einem Schlüssel zu einem Wert und eine andere aus einer Karte von Schlüsseln zu Werten und einem Schlüssel zu einem Wert, wie folgt:
Dann könnten Sie eine Funktion erstellen, die eine Suchfunktion von Schlüssel zu Wert akzeptiert. Sie können eine der oben genannten Nachschlagefunktionen verwenden, die teilweise entweder mit einer Liste oder einer Karte angewendet werden:
Fazit: Currying ist gut, weil Sie damit Funktionen mit einer einfachen Syntax spezialisieren / teilweise anwenden und diese teilweise angewendeten Funktionen dann an Funktionen höherer Ordnung wie
map
oder weitergeben könnenfilter
. Funktionen höherer Ordnung (die Funktionen als Parameter annehmen oder als Ergebnisse liefern) sind das A und O der funktionalen Programmierung, und durch das Ausführen und teilweise angewendete Funktionen können Funktionen höherer Ordnung viel effektiver und präziser verwendet werden.quelle
Die praktische Antwort ist, dass das Erstellen von anonymen Funktionen durch das Currying erheblich vereinfacht wird. Selbst mit einer minimalen Lambda-Syntax ist dies ein Gewinn. vergleichen Sie:
Wenn Sie eine hässliche Lambda-Syntax haben, ist es noch schlimmer. (Ich sehe dich an, JavaScript, Schema und Python.)
Dies wird immer nützlicher, je mehr Funktionen höherer Ordnung verwendet werden. Obwohl ich in Haskell mehr Funktionen höherer Ordnung verwende als in anderen Sprachen, habe ich festgestellt, dass ich die Lambda-Syntax weniger verwende, da in etwa zwei Dritteln der Fälle das Lambda nur eine teilweise angewendete Funktion wäre. (Und die meiste Zeit extrahiere ich es in eine benannte Funktion.)
Grundsätzlich ist es nicht immer offensichtlich, welche Version einer Funktion "kanonisch" ist. Nehmen wir zum Beispiel
map
. Der Typ vonmap
kann auf zwei Arten geschrieben werden:Welches ist das "richtige"? Es ist eigentlich schwer zu sagen. In der Praxis verwenden die meisten Sprachen die erste - map nimmt eine Funktion und eine Liste und gibt eine Liste zurück. Grundsätzlich bildet map jedoch normale Funktionen auf Listenfunktionen ab - es übernimmt eine Funktion und gibt eine Funktion zurück. Wenn eine Karte als Curry-Karte verwendet wird, müssen Sie diese Frage nicht beantworten. Sie erledigt beides auf sehr elegante Weise.
Dies ist besonders wichtig, wenn Sie
map
auf andere Typen als list verallgemeinern .Auch das Curry ist wirklich nicht sehr kompliziert. Es ist eigentlich eine kleine Vereinfachung des Modells, das die meisten Sprachen verwenden: Sie brauchen keine Vorstellung von Funktionen mehrerer Argumente, die in Ihre Sprache eingebettet sind. Dies spiegelt auch die zugrunde liegende Lambda-Rechnung genauer wider.
Natürlich haben ML-artige Sprachen keine Vorstellung von Mehrfachargumenten in Curry- oder nicht-Curry-Form. Die
f(a, b, c)
Syntax entspricht tatsächlich der Übergabe des Tupels(a, b, c)
anf
, nimmt alsof
immer noch nur Argumente an. Dies ist eigentlich eine sehr nützliche Unterscheidung, die ich mir für andere Sprachen gewünscht hätte, da es sehr natürlich ist, so etwas zu schreiben:Mit Sprachen, in denen die Idee mehrerer Argumente steckt, ist dies nicht einfach möglich!
quelle
Das Currying kann nützlich sein, wenn Sie eine Funktion haben, die Sie als erstklassiges Objekt weitergeben, und Sie nicht alle Parameter erhalten, die erforderlich sind, um sie an einer Stelle im Code auszuwerten. Sie können einfach einen oder mehrere Parameter anwenden, wenn Sie sie erhalten, und das Ergebnis an einen anderen Code übergeben, der über mehrere Parameter verfügt, und die Auswertung dort beenden.
Der Code, um dies zu erreichen, wird einfacher sein, als wenn Sie zuerst alle Parameter zusammenführen müssen.
Es besteht auch die Möglichkeit einer weiteren Wiederverwendung von Code, da Funktionen, die einen einzelnen Parameter (eine andere Curry-Funktion) verwenden, nicht so genau mit allen Parametern übereinstimmen müssen.
quelle
Die Hauptmotivation (zumindest anfangs) für das Currying war nicht praktisch, sondern theoretisch. Mit Currying können Sie insbesondere Funktionen mit mehreren Argumenten effektiv abrufen, ohne Semantik für sie oder Semantik für Produkte zu definieren. Dies führt zu einer einfacheren Sprache mit ebenso viel Ausdruckskraft wie eine andere, kompliziertere Sprache und ist daher wünschenswert.
quelle
(Ich werde Beispiele in Haskell geben.)
Wenn Sie funktionale Sprachen verwenden, ist es sehr praktisch, dass Sie eine Funktion teilweise anwenden können. Wie in Haskells
(== x)
ist eine Funktion, die zurückgibt,True
wenn ihr Argument einem bestimmten Ausdruck entsprichtx
:Ohne Curry hätten wir etwas weniger lesbaren Code:
Dies hängt mit der stillschweigenden Programmierung zusammen (siehe auch Pointfree-Stil im Haskell-Wiki). Dieser Stil konzentriert sich nicht auf Werte, die durch Variablen dargestellt werden, sondern auf das Komponieren von Funktionen und den Informationsfluss durch eine Funktionskette. Wir können unser Beispiel in eine Form konvertieren, die überhaupt keine Variablen verwendet:
Hier betrachten wir
==
als eine Funktion vona
bisa -> Bool
undany
als eine Funktion vona -> Bool
bis[a] -> Bool
. Indem wir sie einfach komponieren, erhalten wir das Ergebnis. Dies ist alles dank Currying.In manchen Situationen ist es auch nützlich, das Gegenteil zu tun. Nehmen wir beispielsweise an, wir möchten eine Liste in zwei Teile aufteilen - Elemente, die kleiner als 10 sind, und den Rest und diese beiden Listen dann verketten. Das Aufteilen der Liste erfolgt durch (hier verwenden wir auch Curry ). Das Ergebnis ist von Typ . Anstatt das Ergebnis in seinen ersten und zweiten Teil zu extrahieren und mit zu kombinieren , können wir dies direkt tun, indem wir as entkurbeln
partition
(< 10)
<
([Int],[Int])
++
++
In der Tat
(uncurry (++) . partition (< 10)) [4,12,11,1]
bewertet zu[4,1,12,11]
.Es gibt auch wichtige theoretische Vorteile:
(a, b) -> c
nacha -> (b -> c)
, dass das Ergebnis der letzteren Funktion vom Typ istb -> c
. Mit anderen Worten ist das Ergebnis eine Funktion.quelle
mem x lst = any (\y -> y == x) lst
? (Mit einem Backslash).Currying ist nicht nur syntaktischer Zucker!
Berücksichtigen Sie die
add1
Typensignaturen von (ungecurrt) undadd2
(Curry):(In beiden Fällen sind die Klammern in der Typensignatur optional, ich habe sie jedoch der Übersichtlichkeit halber eingefügt.)
add1
ist eine Funktion, die ein 2-Tupel vonint
und annimmtint
und ein zurückgibtint
.add2
ist eine Funktion, die eine übernimmtint
und eine andere Funktion zurückgibt , die wiederum eine übernimmtint
und eine zurückgibtint
.Der wesentliche Unterschied zwischen den beiden wird deutlicher, wenn wir die Funktionsanwendung explizit angeben. Definieren wir eine Funktion (keine Curry-Funktion), die das erste Argument auf das zweite Argument anwendet:
Jetzt können wir den Unterschied zwischen
add1
undadd2
deutlicher erkennen.add1
wird mit einem 2-Tupel aufgerufen:aber
add2
wird mit einem aufgerufenint
und dann wird sein Rückgabewert mit einem anderen aufgerufenint
:EDIT: Der wesentliche Vorteil des Currys ist, dass Sie eine kostenlose Teilbewerbung erhalten. Nehmen wir an, Sie wollten eine Funktion vom Typ
int -> int
(z. B.map
über eine Liste), die ihrem Parameter 5 hinzufügt. Sie könnten schreibenaddFiveToParam x = x+5
, oder Sie könnten das Äquivalent mit einem Inline-Lambda tun, aber Sie könnten auch viel einfacher schreiben (insbesondere in Fällen, die weniger trivial sind als diese)add2 5
!quelle
Curry ist nur syntaktischer Zucker, aber Sie verstehen etwas falsch, was der Zucker tut, denke ich. Nehmen Sie Ihr Beispiel,
ist eigentlich syntaktischer Zucker für
Das heißt, (add x) gibt eine Funktion zurück, die ein Argument y annimmt und x zu y hinzufügt.
Das ist eine Funktion, die ein Tupel nimmt und seine Elemente hinzufügt. Diese beiden Funktionen sind eigentlich sehr unterschiedlich; Sie vertreten unterschiedliche Argumente.
Wenn Sie 2 zu allen Zahlen in einer Liste hinzufügen möchten:
Das Ergebnis wäre
[3,4,5]
.Wenn Sie hingegen jedes Tupel in einer Liste aufsummieren möchten, passt die Funktion addTuple perfekt.
Das Ergebnis wäre
[12,13,14]
.Curry-Funktionen eignen sich hervorragend für Teilanwendungen, z. B. Map, Fold, App, Filter. Betrachten Sie diese Funktion, die die größte positive Zahl in der angegebenen Liste zurückgibt, oder 0, wenn es keine positiven Zahlen gibt:
quelle
Eine andere Sache, die ich noch nicht erwähnt habe, ist, dass das Currying eine (begrenzte) Abstraktion über die Arität ermöglicht.
Betrachten Sie diese Funktionen, die Teil der Haskell-Bibliothek sind
In jedem Fall kann die Typvariable
c
ein Funktionstyp sein, sodass diese Funktionen mit einem Präfix der Parameterliste ihres Arguments arbeiten. Ohne zu lernen, benötigen Sie entweder ein spezielles Sprachmerkmal, um über Funktionsbereiche zu abstrahieren, oder Sie haben viele verschiedene Versionen dieser Funktionen, die auf verschiedene Bereiche spezialisiert sind.quelle
Mein begrenztes Verständnis ist wie folgt:
1) Teilfunktionsanwendung
Partial Function Application ist das Zurückgeben einer Funktion mit einer geringeren Anzahl von Argumenten. Wenn Sie 2 von 3 Argumenten angeben, wird eine Funktion mit 3-2 = 1 Argument zurückgegeben. Wenn Sie 1 von 3 Argumenten angeben, wird eine Funktion zurückgegeben, die 3-1 = 2 Argumente akzeptiert. Wenn Sie möchten, können Sie sogar 3 von 3 Argumenten teilweise anwenden und es wird eine Funktion zurückgegeben, die kein Argument akzeptiert.
Also gegeben die folgende Funktion:
Wenn Sie 1 an x binden und dies teilweise auf die obige Funktion anwenden,
f(x,y,z)
erhalten Sie:Wo:
f'(y,z) = 1 + y + z;
Wenn Sie nun y an 2 und z an 3 binden und teilweise anwenden, erhalten
f'(y,z)
Sie:Wo:
f''() = 1 + 2 + 3
;Jetzt können Sie jederzeit auswählen, ob oder ausgewertet werden
f
soll . Also kann ich tun:f'
f''
oder
2) Currying
Currying auf der anderen Seite ist das Verfahren , eine Funktion in einer verschachtelten Kette von einem Argument Funktionen aufzuteilen. Sie können niemals mehr als ein Argument angeben, es ist eins oder null.
Also gegeben die gleiche Funktion:
Wenn Sie es curryen, würden Sie eine Kette von 3 Funktionen erhalten:
Wo:
Wenn Sie jetzt anrufen
f'(x)
mitx = 1
:Sie erhalten eine neue Funktion zurück:
Wenn Sie anrufen
g(y)
mity = 2
:Sie erhalten eine neue Funktion zurück:
Zum Schluss, wenn Sie anrufen
h(z)
mitz = 3
:Sie werden zurückgebracht
6
.3) Schließung
Schließlich ist Closure der Vorgang, bei dem eine Funktion und Daten als eine Einheit erfasst werden. Ein Funktionsabschluss kann 0 bis unendlich viele Argumente annehmen, berücksichtigt jedoch auch Daten, die nicht an ihn übergeben wurden.
Wieder mit der gleichen Funktion:
Sie können stattdessen einen Abschluss schreiben:
Wo:
f'
wird geschlossenx
. Das bedeutet, dassf'
der Wert von x gelesen werden kann, der sich darin befindetf
.Also, wenn Sie anrufen würden
f
mitx = 1
:Sie würden eine Schließung bekommen:
Wenn Sie jetzt
closureOfF
mity = 2
und angerufen habenz = 3
:Welches würde zurückkehren
6
Fazit
Currying, Teilapplikation und Verschlüsse ähneln sich insofern, als sie eine Funktion in mehrere Teile zerlegen.
Durch das Ausführen wird eine Funktion mehrerer Argumente in verschachtelte Funktionen einzelner Argumente zerlegt, die Funktionen einzelner Argumente zurückgeben. Es hat keinen Sinn, eine Funktion mit einem oder weniger Argumenten aufzurufen, da dies keinen Sinn ergibt.
Partielle Anwendung zerlegt eine Funktion mehrerer Argumente in eine Funktion kleinerer Argumente, deren jetzt fehlende Argumente den angegebenen Wert ersetzt haben.
Closure zerlegt eine Funktion in eine Funktion und ein Dataset, wobei Variablen innerhalb der Funktion, die nicht übergeben wurden, in das Dataset schauen können, um einen Wert zu finden, an den sie gebunden werden können, wenn sie zur Auswertung aufgefordert werden.
Was an all diesen verwirrend ist, ist, dass sie verwendet werden können, um eine Teilmenge der anderen zu implementieren. Im Grunde sind sie alle ein kleines Implementierungsdetail. Sie alle bieten einen ähnlichen Wert, da Sie nicht alle Werte im Voraus erfassen müssen und einen Teil der Funktion wiederverwenden können, da Sie sie in diskrete Einheiten zerlegt haben.
Offenlegung
Ich bin auf keinen Fall ein Experte des Themas, ich habe erst vor kurzem angefangen, darüber zu lernen, und daher gebe ich mein derzeitiges Verständnis wieder, aber es könnte Fehler geben, auf die ich Sie hinweisen möchte, und ich werde korrigieren als / ob Ich entdecke keine.
quelle
Mit Currying (Teilanwendung) können Sie eine neue Funktion aus einer vorhandenen Funktion erstellen, indem Sie einige Parameter festlegen. Es ist ein Sonderfall des lexikalischen Abschlusses, bei dem die anonyme Funktion nur ein trivialer Wrapper ist, der einige erfasste Argumente an eine andere Funktion weitergibt. Wir können dies auch tun, indem wir die allgemeine Syntax verwenden, um lexikalische Abschlüsse zu machen, aber eine teilweise Anwendung liefert einen vereinfachten syntaktischen Zucker.
Aus diesem Grund verwenden Lisp-Programmierer manchmal Bibliotheken für Teilanwendungen , wenn sie in einem funktionalen Stil arbeiten .
Anstatt
(lambda (x) (+ 3 x))
, was uns eine Funktion gibt, die 3 zu ihrem Argument hinzufügt, können Sie so etwas wie schreiben(op + 3)
, und 3 zu jedem Element einer Liste hinzuzufügen, wäre dann(mapcar (op + 3) some-list)
eher als(mapcar (lambda (x) (+ 3 x)) some-list)
. Diesesop
Makro macht Sie zu einer Funktion, die einige Argumentex y z ...
akzeptiert und aufruft(+ a x y z ...)
.In vielen rein funktionalen Sprachen ist eine teilweise Anwendung in der Syntax verankert, sodass es keinen
op
Operator gibt. Um eine Teilanwendung auszulösen, rufen Sie einfach eine Funktion mit weniger Argumenten auf, als sie benötigt. Anstatt einen"insufficient number of arguments"
Fehler zu erzeugen , ist das Ergebnis eine Funktion der verbleibenden Argumente.quelle
a -> b -> c
hat keinen Parameter s (Plural), sondern nur einen Parameterc
. Beim Aufruf wird eine Funktion vom Typ zurückgegebena -> b
.Für die Funktion
Es ist von der Form
f': 'a * 'b -> 'c
Um zu bewerten, wird man tun
Für die Curry-Funktion
Um zu bewerten, wird man tun
Wo es sich um eine Teilberechnung handelt, nämlich (3 + y), mit der man dann die Berechnung abschließen kann
hinzufügen im zweiten Fall ist von der Form
f: 'a -> 'b -> 'c
Das Currying wandelt hier eine Funktion um, die zwei Vereinbarungen in eine umsetzt, von denen nur eine ein Ergebnis zurückgibt. Teilbewertung
Sagen wir
x
auf der RHS ist nicht nur ein regulärer int, sondern eine komplexe Berechnung, die eine Weile dauert, um zwei Sekunden zu vervollständigen.So sieht die Funktion nun aus
Vom Typ
add : int * int -> int
Jetzt wollen wir diese Funktion für einen Bereich von Zahlen berechnen, lassen Sie uns sie abbilden
Für das oben Gesagte
twoSecondsComputation
wird das Ergebnis jedes Mal ausgewertet. Dies bedeutet, dass diese Berechnung 6 Sekunden dauert.Durch die Kombination von Staging und Currying kann dies vermieden werden.
Von der Curryform
add : int -> int -> int
Jetzt kann man tun,
Das muss
twoSecondsComputation
nur einmal ausgewertet werden. Ersetzen Sie zum Erhöhen der Skala zwei Sekunden durch 15 Minuten oder eine beliebige Stunde, und erstellen Sie dann eine Karte mit 100 Zahlen.Zusammenfassung : Currying ist großartig, wenn es mit anderen Methoden für übergeordnete Funktionen als Werkzeug für die Teilbewertung verwendet wird. Sein Zweck kann von sich aus nicht wirklich bewiesen werden.
quelle
Das Currying ermöglicht eine flexible Funktionszusammensetzung.
Ich habe eine Funktion "Curry" erfunden. In diesem Zusammenhang ist es mir egal, welche Art von Logger ich bekomme oder woher er kommt. Es ist mir egal, was die Aktion ist oder woher sie kommt. Alles, was mich interessiert, ist die Verarbeitung meiner Eingaben.
Die Buildervariable ist eine Funktion, die eine Funktion zurückgibt, die meine Eingaben für meine Arbeit übernimmt. Dies ist ein einfaches nützliches Beispiel und kein Objekt in Sicht.
quelle
Currying ist ein Vorteil, wenn Sie nicht alle Argumente für eine Funktion haben. Wenn Sie die Funktion vollständig auswerten, gibt es keinen signifikanten Unterschied.
Durch das Currying können Sie die Erwähnung noch nicht benötigter Parameter vermeiden. Es ist prägnanter und erfordert nicht das Auffinden eines Parameternamens, der nicht mit einer anderen Variablen im Gültigkeitsbereich kollidiert (was mein Lieblingsvorteil ist).
Wenn Sie beispielsweise Funktionen verwenden, die Funktionen als Argumente verwenden, befinden Sie sich häufig in Situationen, in denen Sie Funktionen wie "3 zur Eingabe hinzufügen" oder "Eingabe mit Variable v vergleichen" benötigen. Mit currying werden diese Funktionen leicht geschrieben:
add 3
und(== v)
. Ohne Curry müssen Sie Lambda-Ausdrücke verwenden:x => add 3 x
undx => x == v
. Die Lambda-Ausdrücke sind doppelt so lang und haben eine kleine Menge an Arbeit im Zusammenhang mit der Auswahl eines Namens, außerx
wenn es bereits einenx
Gültigkeitsbereich gibt.Ein Nebeneffekt von auf Curry basierenden Sprachen ist, dass Sie beim Schreiben von generischem Code für Funktionen nicht mit Hunderten von Varianten auf der Basis der Anzahl der Parameter enden. In C # würde eine Curry-Methode beispielsweise Varianten für Func <R>, Func <A, R>, Func <A1, A2, R>, Func <A1, A2, A3, R> usw. benötigen für immer. In Haskell ähnelt das Äquivalent einer Func <A1, A2, R> eher einer Func <Tuple <A1, A2>, R> oder einer Func <A1, Func <A2, R >> (und einer Func <R>) entspricht eher einer Func <Unit, R>), sodass alle Varianten dem einzelnen Func <A, R> -Fall entsprechen.
quelle
Die primäre Überlegung, die ich mir vorstellen kann (und die ich in diesem Bereich keinesfalls als Experte bezeichne), zeigt allmählich ihre Vorteile, wenn sich die Funktionen von trivial zu nicht trivial bewegen. In allen trivialen Fällen mit den meisten Konzepten dieser Art werden Sie keinen wirklichen Nutzen finden. In den meisten funktionalen Sprachen wird der Stapel jedoch bei Verarbeitungsvorgängen häufig verwendet. Betrachten Sie dazu beispielsweise PostScript oder Lisp . Durch die Verwendung von Currying können Funktionen effektiver gestapelt werden, und dieser Vorteil wird deutlich, wenn die Operationen immer weniger trivial werden. In der gewohnten Weise können der Befehl und die Argumente der Reihe nach auf den Stapel gelegt und nach Bedarf entfernt werden, damit sie in der richtigen Reihenfolge ausgeführt werden.
quelle
Das Currying hängt entscheidend (definitiv sogar) von der Fähigkeit ab, eine Funktion zurückzugeben.
Betrachten Sie diesen (erfundenen) Pseudocode.
var f = (m, x, b) => ... etwas zurückgeben ...
Nehmen wir an, dass der Aufruf von f mit weniger als drei Argumenten eine Funktion zurückgibt.
var g = f (0,1); // Dies gibt eine an 0 und 1 gebundene Funktion (m und x) zurück, die ein weiteres Argument (b) akzeptiert.
var y = g (42); // rufe g mit dem fehlenden dritten Argument auf und benutze 0 und 1 für m und x
Dass Sie teilweise Argumente anwenden und eine wiederverwendbare Funktion zurückerhalten können (die an die von Ihnen angegebenen Argumente gebunden ist), ist sehr nützlich (und DRY).
quelle