Was ist der Vorteil des Currys?


154

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?


54
Alleine zu covern ist im Grunde genommen nutzlos, aber wenn alle Funktionen standardmäßig covern sind, sind viele andere Funktionen viel nützlicher. Es ist schwer zu verstehen, bis Sie tatsächlich eine funktionierende Sprache für eine Weile verwendet haben.
CA McCann

4
Etwas, das im Vorbeigehen von Delnan in einem Kommentar zu JoelEthertons Antwort erwähnt wurde, das ich aber ausdrücklich erwähnen wollte, ist, dass man (zumindest in Haskell) nicht nur Funktionen, sondern auch Typkonstruktoren zum Teil anwenden kann - das kann durchaus sein praktisch; Dies könnte etwas zum Nachdenken sein.
Paul

Alle haben Beispiele für Haskell gegeben. Man könnte sich fragen, ob Curry nur in Haskell nützlich ist.
Manoj R

@ManojR Alle haben in Haskell keine Beispiele angegeben.
Phwd

1
Die Frage löste eine interessante Diskussion über Reddit aus .
Yannis

Antworten:


126

Mit Curry-Funktionen können Sie abstraktere Funktionen einfacher wiederverwenden, da Sie sich spezialisieren können. Angenommen, Sie haben eine Additionsfunktion

add x y = x + y

und dass Sie jedem Mitglied einer Liste 2 hinzufügen möchten. In Haskell würden Sie dies tun:

map (add 2) [1, 2, 3] -- gives [3, 4, 5]
-- actually one could just do: map (2+) [1, 2, 3], but that may be Haskell specific

Hier ist die Syntax leichter als wenn Sie eine Funktion erstellen müssten add2

add2 y = add 2 y
map add2 [1, 2, 3]

oder wenn Sie eine anonyme Lambda-Funktion machen mussten:

map (\y -> 2 + y) [1, 2, 3]

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:

lookup1 :: [(Key, Value)] -> Key -> Value -- or perhaps it should be Maybe Value
lookup2 :: Map Key Value -> Key -> Value

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:

myFunc :: (Key -> Value) -> .....

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 mapoder weitergeben können filter. 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.


31
Es ist erwähnenswert, dass aus diesem Grund die für Funktionen in Haskell verwendete Argumentreihenfolge häufig davon abhängt, wie wahrscheinlich eine teilweise Anwendung ist, was wiederum dazu führt, dass die oben beschriebenen Vorteile in mehr Situationen gelten (ha, ha). Das Standardcurrying ist daher sogar noch vorteilhafter, als dies anhand konkreter Beispiele wie hier ersichtlich ist.
CA McCann

wat. "Einer aus einer Liste von Schlüssel / Wert-Paaren und einem Schlüssel zu einem Wert und ein anderer aus einer Karte von Schlüsseln zu Werten und einem Schlüssel zu einem Wert"
Mateen Ulhaq

@MateenUlhaq Es ist eine Fortsetzung des vorherigen Satzes, in dem wir einen Wert basierend auf einem Schlüssel erhalten möchten, und wir haben zwei Möglichkeiten, dies zu tun. Der Satz zählt diese beiden Möglichkeiten auf. Auf die erste Art und Weise erhalten Sie eine Liste von Schlüssel / Wert-Paaren und den Schlüssel, für den wir den Wert finden möchten, und auf die andere Art und Weise erhalten wir eine ordnungsgemäße Zuordnung und erneut einen Schlüssel. Es kann hilfreich sein, sich den Code unmittelbar nach dem Satz anzusehen.
Boris

53

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:

map (add 1) [1..10]
map (\ x -> add 1 x) [1..10]

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 von mapkann auf zwei Arten geschrieben werden:

map :: (a -> b) -> [a] -> [b]
map :: (a -> b) -> ([a] -> [b])

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 mapauf 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)an f, nimmt also fimmer 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:

map f [(1,2,3), (4,5,6), (7, 8, 9)]

Mit Sprachen, in denen die Idee mehrerer Argumente steckt, ist dies nicht einfach möglich!


1
"Sprachen im ML-Stil haben keine Vorstellung von Mehrfachargumenten in Curry- oder nicht-Curry-Form": Ist diesbezüglich der ML-Stil von Haskell?
Giorgio

1
@ Giorgio: Ja.
Tikhon Jelvis

1
Interessant. Ich kenne Haskell und lerne gerade SML. Es ist also interessant, Unterschiede und Ähnlichkeiten zwischen den beiden Sprachen festzustellen.
Giorgio

Gute Antwort, und wenn Sie immer noch nicht überzeugt sind, denken Sie an Unix-Pipelines, die Lambda-Streams ähneln
Sridhar Sarnobat

Die "praktische" Antwort ist wenig relevant, da die Ausführlichkeit in der Regel durch teilweises Anwenden vermieden wird und nicht durch das Currying. Und ich würde hier argumentieren, dass die Syntax der Lambda-Abstraktion (trotz der Typdeklaration) hässlicher ist als die in Schema (zumindest), da sie mehr eingebaute spezielle syntaktische Regeln benötigt, um sie korrekt zu analysieren, wodurch die Sprachspezifikation ohne Gewinn aufgebläht wird über semantische Eigenschaften.
FrankHB

24

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.


14

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.


2
Während die Motivation hier theoretisch ist, denke ich, dass Einfachheit fast immer auch ein praktischer Vorteil ist. Wenn ich mir keine Gedanken über Funktionen mit mehreren Argumenten mache, wird mir das Programmieren einfacher, so als würde ich mit Semantik arbeiten.
Tikhon Jelvis

2
@TikhonJelvis Wenn Sie jedoch programmieren, gibt Ihnen das Curry andere Sorgen, z. B. dass der Compiler die Tatsache, dass Sie zu wenig Argumente an eine Funktion übergeben haben, nicht bemerkt oder in diesem Fall sogar eine schlechte Fehlermeldung erhält. Wenn Sie nicht mit Curry arbeiten, ist der Fehler viel offensichtlicher.
Alex R

Ich hatte noch nie solche Probleme: GHC ist zumindest in dieser Hinsicht sehr gut. Der Compiler erkennt solche Probleme immer und hat auch für diesen Fehler gute Fehlermeldungen.
Tikhon Jelvis

1
Ich kann nicht zustimmen, dass die Fehlermeldungen als gut qualifiziert sind. Wartungsfähig, ja, aber sie sind noch nicht gut. Diese Art von Problem wird auch nur erkannt, wenn es zu einem Tippfehler kommt, dh wenn Sie später versuchen, das Ergebnis als eine andere Funktion zu verwenden (oder wenn Sie einen Kommentar eingegeben haben, aber wenn Sie sich bei lesbaren Fehlern darauf verlassen, hat dies seine eigenen Probleme ); Der gemeldete Ort des Fehlers ist vom tatsächlichen Ort getrennt.
Alex R

14

(Ich werde Beispiele in Haskell geben.)

  1. 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, Truewenn ihr Argument einem bestimmten Ausdruck entspricht x:

    mem :: Eq a => a -> [a] -> Bool
    mem x lst = any (== x) lst
    

    Ohne Curry hätten wir etwas weniger lesbaren Code:

    mem x lst = any (\y -> y == x) lst
    
  2. 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:

    mem = any . (==)
    

    Hier betrachten wir ==als eine Funktion von abis a -> Boolund anyals eine Funktion von a -> Boolbis [a] -> Bool. Indem wir sie einfach komponieren, erhalten wir das Ergebnis. Dies ist alles dank Currying.

  3. 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 entkurbelnpartition (< 10)<([Int],[Int])++++

    uncurry (++) . partition (< 10)
    

In der Tat (uncurry (++) . partition (< 10)) [4,12,11,1]bewertet zu [4,1,12,11].

Es gibt auch wichtige theoretische Vorteile:

  1. Currying ist für Sprachen, denen Datentypen fehlen und die nur Funktionen haben, wie zum Beispiel die Lambda-Rechnung, unerlässlich . Während diese Sprachen für den praktischen Gebrauch nicht nützlich sind, sind sie aus theoretischer Sicht sehr wichtig.
  2. Dies hängt mit der wesentlichen Eigenschaft funktionaler Sprachen zusammen - Funktionen sind erstklassige Objekte. Wie wir gesehen haben, bedeutet die Konvertierung von (a, b) -> cnach a -> (b -> c), dass das Ergebnis der letzteren Funktion vom Typ ist b -> c. Mit anderen Worten ist das Ergebnis eine Funktion.
  3. (Un) currying ist eng mit kartesischen geschlossenen Kategorien verbunden , was eine kategorische Sichtweise auf typisierte Lambda-Kalküle darstellt.

Sollte das nicht für das "viel weniger lesbare Code" -Bit sein mem x lst = any (\y -> y == x) lst? (Mit einem Backslash).
Stusmith

Ja, danke, dass Sie darauf hingewiesen haben, ich werde es korrigieren.
Petr Pudlák

9

Currying ist nicht nur syntaktischer Zucker!

Berücksichtigen Sie die add1Typensignaturen von (ungecurrt) und add2(Curry):

add1 : (int * int) -> int
add2 : int -> (int -> int)

(In beiden Fällen sind die Klammern in der Typensignatur optional, ich habe sie jedoch der Übersichtlichkeit halber eingefügt.)

add1ist eine Funktion, die ein 2-Tupel von intund annimmt intund ein zurückgibt int. add2ist eine Funktion, die eine übernimmt intund eine andere Funktion zurückgibt , die wiederum eine übernimmt intund eine zurückgibt int.

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:

apply(f, b) = f b

Jetzt können wir den Unterschied zwischen add1und add2deutlicher erkennen. add1wird mit einem 2-Tupel aufgerufen:

apply(add1, (3, 5))

aber add2wird mit einem aufgerufen int und dann wird sein Rückgabewert mit einem anderen aufgerufenint :

apply(apply(add2, 3), 5)

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 schreiben addFiveToParam 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!


3
Ich verstehe, dass es für mein Beispiel einen großen Unterschied hinter den Kulissen gibt, aber das Ergebnis scheint eine einfache syntaktische Änderung zu sein.
Mad Scientist

5
Currying ist kein sehr tiefes Konzept. Es geht um die Vereinfachung des zugrunde liegenden Modells (siehe Lambda-Berechnung) oder in Sprachen, die ohnehin Tupel haben, es geht tatsächlich um die syntaktische Bequemlichkeit einer teilweisen Anwendung. Unterschätzen Sie nicht die Bedeutung der syntaktischen Bequemlichkeit.
Peaker

9

Curry ist nur syntaktischer Zucker, aber Sie verstehen etwas falsch, was der Zucker tut, denke ich. Nehmen Sie Ihr Beispiel,

fun add x y = x + y

ist eigentlich syntaktischer Zucker für

fun add x = fn y => x + y

Das heißt, (add x) gibt eine Funktion zurück, die ein Argument y annimmt und x zu y hinzufügt.

fun addTuple (x, y) = x + y

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:

(* add 2 to all numbers using the uncurried function *)
map (fn x => addTuple (x, 2)) [1,2,3]
(* using the curried function *)
map (add 2) [1,2,3]

Das Ergebnis wäre [3,4,5].

Wenn Sie hingegen jedes Tupel in einer Liste aufsummieren möchten, passt die Funktion addTuple perfekt.

(* Sum each tuple using the uncurried function *)
map addTuple [(10,2), (10,3), (10,4)]    
(* sum each tuple using curried function *)
map (fn (a,b) => add a b) [(10,2), (10,3), (10,4)]

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:

- val highestPositive = foldr Int.max 0;   
val highestPositive = fn : int list -> int 

1
Ich habe verstanden, dass die Curry-Funktion eine andere Typensignatur hat und dass es sich tatsächlich um eine Funktion handelt, die eine andere Funktion zurückgibt. Mir fehlte allerdings der Teilbewerbungsteil.
Mad Scientist

9

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

(.) :: (b -> c) -> (a -> b) -> a -> c
either :: (a -> c) -> (b -> c) -> Either a b -> c
flip :: (a -> b -> c) -> b -> a -> c
on :: (b -> b -> c) -> (a -> b) -> a -> a -> c

In jedem Fall kann die Typvariable cein 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.


6

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:

f(x,y,z) = x + y + z;

Wenn Sie 1 an x ​​binden und dies teilweise auf die obige Funktion anwenden, f(x,y,z)erhalten Sie:

f(1,y,z) = f'(y,z);

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:

f'(2,3) = f''();

Wo: f''() = 1 + 2 + 3;

Jetzt können Sie jederzeit auswählen, ob oder ausgewertet werden fsoll . Also kann ich tun:f'f''

print(f''()) // and it would return 6;

oder

print(f'(1,1)) // and it would return 3;

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:

f(x,y,z) = x + y + z;

Wenn Sie es curryen, würden Sie eine Kette von 3 Funktionen erhalten:

f'(x) -> f''(y) -> f'''(z)

Wo:

f'(x) = x + f''(y);

f''(y) = y + f'''(z);

f'''(z) = z;

Wenn Sie jetzt anrufen f'(x)mit x = 1:

f'(1) = 1 + f''(y);

Sie erhalten eine neue Funktion zurück:

g(y) = 1 + f''(y);

Wenn Sie anrufen g(y)mit y = 2:

g(2) = 1 + 2 + f'''(z);

Sie erhalten eine neue Funktion zurück:

h(z) = 1 + 2 + f'''(z);

Zum Schluss, wenn Sie anrufen h(z)mit z = 3:

h(3) = 1 + 2 + 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:

f(x,y,z) = x + y + z;

Sie können stattdessen einen Abschluss schreiben:

f(x) = x + f'(y, z);

Wo:

f'(y,z) = x + y + z;

f'wird geschlossen x. Das bedeutet, dass f'der Wert von x gelesen werden kann, der sich darin befindet f.

Also, wenn Sie anrufen würden fmit x = 1:

f(1) = 1 + f'(y, z);

Sie würden eine Schließung bekommen:

closureOfF(y, z) =
                   var x = 1;
                   f'(y, z);

Wenn Sie jetzt closureOfFmit y = 2und angerufen haben z = 3:

closureOfF(2, 3) = 
                   var x = 1;
                   x + 2 + 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.


1
Die Antwort lautet also: Currying hat keinen Vorteil?
13.

1
@ceving Soweit ich weiß, ist das richtig. In der Praxis bieten Currying und Teilanwendung die gleichen Vorteile. Die Wahl, welche Sprache implementiert werden soll, wird aus Implementierungsgründen getroffen. Bei einer bestimmten Sprache ist eine Implementierung möglicherweise einfacher als eine andere.
Didier A.

5

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). Dieses opMakro macht Sie zu einer Funktion, die einige Argumente x 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 opOperator 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.


"Mit Currying ... können Sie eine neue Funktion erstellen ... indem Sie einige Parameter korrigieren" - nein, eine Funktion vom Typ a -> b -> chat keinen Parameter s (Plural), sondern nur einen Parameter c. Beim Aufruf wird eine Funktion vom Typ zurückgegeben a -> b.
Max Heiber

4

Für die Funktion

fun add(x, y) = x + y

Es ist von der Form f': 'a * 'b -> 'c

Um zu bewerten, wird man tun

add(3, 5)
val it = 8 : int

Für die Curry-Funktion

fun add x y = x + y

Um zu bewerten, wird man tun

add 3
val it = fn : int -> int

Wo es sich um eine Teilberechnung handelt, nämlich (3 + y), mit der man dann die Berechnung abschließen kann

it 5
val it = 8 : int

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

Warum sollte man das brauchen?

Sagen wir xauf der RHS ist nicht nur ein regulärer int, sondern eine komplexe Berechnung, die eine Weile dauert, um zwei Sekunden zu vervollständigen.

x = twoSecondsComputation(z)

So sieht die Funktion nun aus

fun add (z:int) (y:int) : int =
    let
        val x = twoSecondsComputation(z)
    in
        x + y
    end;

Vom Typ add : int * int -> int

Jetzt wollen wir diese Funktion für einen Bereich von Zahlen berechnen, lassen Sie uns sie abbilden

val result1 = map (fn x => add (20, x)) [3, 5, 7];

Für das oben Gesagte twoSecondsComputationwird das Ergebnis jedes Mal ausgewertet. Dies bedeutet, dass diese Berechnung 6 Sekunden dauert.

Durch die Kombination von Staging und Currying kann dies vermieden werden.

fun add (z:int) : int -> int =
    let
        val x = twoSecondsComputation(z)
    in
        (fn y => x + y)
    end;

Von der Curryform add : int -> int -> int

Jetzt kann man tun,

val add' = add 20;
val result2 = map add' [3, 5, 7, 11, 13];

Das muss twoSecondsComputationnur 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.


3

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.

var builder = curry(function(input, logger, action) {
     logger.log("Starting action");
     try {
         action(input);
         logger.log("Success!");
     }
     catch (err) {
         logger.logerror("Boo we failed..", err);
     }
});
var x = "My input.";
goGatherArgs(builder)(x); // Supplies action first, then logger somewhere.

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.


2

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 3und (== v). Ohne Curry müssen Sie Lambda-Ausdrücke verwenden: x => add 3 xund x => 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ßer xwenn es bereits einen xGü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.


2

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.


1
Wie genau macht es die Sache effizienter, wenn wesentlich mehr Stack-Frames erstellt werden müssen?
Mason Wheeler

1
@MasonWheeler: Ich würde nicht wissen, wie ich sagte, dass ich kein Experte für funktionale Sprachen oder spezielles Currying bin. Ich habe dieses Community-Wiki deshalb speziell gekennzeichnet.
Joel Etherton

4
@MasonWheeler Sie haben Recht mit der Formulierung dieser Antwort, aber lassen Sie mich einschneiden und sagen, dass die Anzahl der tatsächlich erstellten Stack-Frames stark von der Implementierung abhängt. Beispielsweise verzögert in der spineless tagless G-Maschine (STG; die Art und Weise, wie GHC Haskell implementiert) die tatsächliche Auswertung, bis alle (oder mindestens so viele, wie bekanntermaßen erforderlich) Argumente akkumuliert sind. Ich kann mich anscheinend nicht erinnern, ob dies für alle Funktionen oder nur für Konstruktoren durchgeführt wird, aber ich denke, es sollte für die meisten Funktionen möglich sein. (

1

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).

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.