Nennt groovy "Currying" für Teilanwendungen?

15

Groovy hat ein Konzept, das es "Curry" nennt. Hier ist ein Beispiel aus ihrem Wiki:

def divide = { a, b -> a / b }

def halver = divide.rcurry(2)

assert halver(8) == 4

Ich verstehe, was hier vor sich geht, dass das Argument der rechten Hand dividean den Wert 2 gebunden ist. Dies scheint eine Form der Teilanwendung zu sein.

Der Begriff currying wird normalerweise verwendet, um eine Funktion, die eine Reihe von Argumenten verwendet, in eine Funktion umzuwandeln, die nur ein Argument verwendet und eine andere Funktion zurückgibt. Zum Beispiel ist hier der Typ der curryFunktion in Haskell:

curry :: ((a, b) -> c) -> (a -> (b -> c))

Für Leute, die Haskell noch nicht benutzt ahaben bund cgenerische Parameter sind. curryNimmt eine Funktion mit zwei Argumenten und gibt eine Funktion zurück, die aeine Funktion von bbis annimmt und zurückgibt c. Ich habe dem Typ ein zusätzliches Paar Klammern hinzugefügt, um dies deutlicher zu machen.

Habe ich falsch verstanden, was im groovigen Beispiel vor sich geht, oder ist es nur eine falsch benannte Teilanwendung? Oder macht es tatsächlich beides: Das heißt, konvertieren dividein eine Curry-Funktion und dann teilweise 2auf diese neue Funktion anwenden .

Richard Warburton
quelle
1
für diejenigen, die kein haskell sprechen msmvps.com/blogs/jon_skeet/archive/2012/01/30/…
jk.

Antworten:

14

Groovys Implementierung von currycurry nicht einmal hinter den Kulissen. Es ist im Wesentlichen identisch mit der Teilanwendung.

Die Methoden curry, rcurryund geben ein Objekt zurück , das die gebundenen Argumente enthält. Es gibt auch eine Methode (falsch benannt - Sie verwenden Curry-Funktionen, keine Argumente), die die Zusammensetzung der Argumente zurückgibt, die mit den gebundenen Argumenten an sie übergeben wurden.ncurryCurriedClosuregetUncurriedArguments

Wenn ein Closure aufgerufen wird, ruft es letztendlich die invokeMethodMethode von aufMetaClassImpl , die explizit prüft, ob das aufrufende Objekt eine Instanz von ist CurriedClosure. In diesem Fall werden die oben genannten getUncurriedArgumentsInformationen verwendet , um die gesamte Anzahl der anzuwendenden Argumente zusammenzustellen:

if (objectClass == CurriedClosure.class) {
    // ...
    final Object[] curriedArguments = cc.getUncurriedArguments(arguments);
    // [Ed: Yes, you read that right, curried = uncurried. :) ]
    // ...
    return ownerMetaClass.invokeMethod(owner, methodName, curriedArguments);
}

Aufgrund der verwirrenden und etwas inkonsistenten obigen Nomenklatur vermute ich, dass derjenige, der dies geschrieben hat, ein gutes konzeptuelles Verständnis hat, aber vielleicht ein wenig überstürzt war und - wie viele kluge Leute - das Currying mit einer teilweisen Anwendung in Konflikt brachte. Dies ist verständlich (siehe Antwort von Paul King), wenn auch etwas unglücklich; Es wird schwierig sein, dies zu korrigieren, ohne die Abwärtskompatibilität zu beeinträchtigen.

Eine Lösung, die ich vorgeschlagen habe, besteht darin, die curryMethode so zu überladen , dass sie, wenn keine Argumente übergeben werden, tatsächlich ausgeführt wird , und den Aufruf der Methode mit Argumenten zugunsten einer neuen partialFunktion nicht mehr zu empfehlen . Dies mag ein wenig seltsam erscheinen , würde jedoch die Abwärtskompatibilität maximieren, da es keinen Grund gibt, eine Teilanwendung mit Nullargumenten zu verwenden, und gleichzeitig die (IMHO) hässlichere Situation vermeiden, eine neue, anders benannte Funktion für das ordnungsgemäße Aufrufen zu haben, während die Funktion tatsächlich ausgeführt wird named currymacht etwas anderes und verwirrend ähnliches.

Es versteht sich von selbst, dass sich das Ergebnis eines Anrufs curryvöllig vom tatsächlichen Currying unterscheidet. Wenn die Funktion wirklich funktioniert, können Sie schreiben:

def add = { x, y -> x + y }
def addCurried = add.curry()   // should work like { x -> { y -> x + y } }
def add1 = addCurried(1)       // should work like { y -> 1 + y }
assert add1(1) == 2 

... und es würde funktionieren, denn addCurriedsollte funktionieren wie { x -> { y -> x + y } }. Stattdessen wird eine Laufzeitausnahme ausgelöst und Sie sterben ein wenig im Inneren.

Jordan Gray
quelle
1
Ich denke, dass rcurry und ncurry auf Funktionen mit Argumenten> 2 zeigen, dass dies wirklich nur eine Teilanwendung ist, die nicht funktioniert
jk.
@jk Tatsächlich ist es auf Funktionen mit Argumenten == 2 nachweisbar, wie ich am Ende notiere. :)
Jordan Gray
3
@matcauthon Genau genommen besteht der "Zweck" des Lehrgangs darin, eine Funktion mit vielen Argumenten in eine verschachtelte Funktionskette mit jeweils einem Argument umzuwandeln. Ich denke, was Sie verlangen, ist ein praktischer Grund, warum Sie Curry verwenden möchten , was in Groovy etwas schwieriger zu rechtfertigen ist als in zB LISP oder Haskell. Der Punkt ist, was Sie wahrscheinlich die meiste Zeit verwenden möchten, ist Teilanwendung, nicht Currying.
Jordan Gray
4
+1 fürand you die a little inside
Thomas Eding
1
Wow, ich verstehe das Curry viel besser, nachdem ich deine Antwort gelesen habe: +1 und danke! Spezifisch für die Frage, gefällt mir, dass Sie sagten, "Curry mit teilweiser Anwendung verschmolzen."
GlenPeterson
3

Ich denke, es ist klar, dass grooviges Curry tatsächlich eine Teilanwendung ist, wenn Funktionen mit mehr als zwei Argumenten betrachtet werden. Erwägen

f :: (a,b,c) -> d

seine Curryform wäre

fcurried :: a -> b -> c -> d

Das Curry von Groovy gibt jedoch etwas zurück, das dem entspricht (vorausgesetzt, es wird mit 1 Argument x aufgerufen).

fgroovy :: (b,c) -> d 

welches f mit dem Wert von a fest auf x aufruft

Das heißt, während Groovys Curry Funktionen mit N-1 Argumenten zurückgeben kann, haben Curry-Funktionen eigentlich immer nur 1 Argument, daher kann Groovy nicht mit Curry curren

jk.
quelle
2

Groovy übernahm die Benennung seiner Curry-Methoden aus zahlreichen anderen nicht reinen FP-Sprachen, die ebenfalls ähnliche Benennungen für Teilanwendungen verwenden - was für eine solche FP-zentrierte Funktionalität möglicherweise unglücklich ist. Es werden mehrere "echte" Currying-Implementierungen zur Aufnahme in Groovy vorgeschlagen. Ein guter Thread, um darüber zu lesen, ist hier:

http://groovy.markmail.org/thread/c4ycxdzm3ack6xxb

Die vorhandene Funktionalität bleibt in irgendeiner Form erhalten und die Abwärtskompatibilität wird berücksichtigt, wenn aufgerufen wird, wie die neuen Methoden benannt werden sollen usw. - daher kann ich zum gegenwärtigen Zeitpunkt nicht sagen, wie die endgültigen Namen der neuen / alten Methoden lauten sollen Sein. Wahrscheinlich ein Kompromiss bei der Benennung, aber wir werden sehen.

Für die meisten OO-Programmierer ist die Unterscheidung zwischen den beiden Begriffen (Currying und Teilanwendung) wohl weitgehend akademisch; Sobald Sie sich jedoch an diese gewöhnt haben (und wer auch immer Ihren Code warten wird, ist darin geschult, diese Art der Codierung zu lesen), können Sie bestimmte Arten von Algorithmen durch punktfreie oder stillschweigende Programmierung (die von "echten" Lehrmethoden unterstützt wird) kompakter ausdrücken und in einigen Fällen eleganter. Hier liegt offensichtlich etwas "Schönheit in den Augen des Betrachters", aber die Fähigkeit, beide Stile zu unterstützen, liegt in der Natur von Groovy (OO / FP, statisch / dynamisch, Klassen / Skripte usw.).

Paul King
quelle
1

In Anbetracht dieser bei IBM gefundenen Definition:

Der Begriff Curry stammt von Haskell Curry, dem Mathematiker, der das Konzept der Teilfunktionen entwickelt hat. Currying bezieht sich darauf, dass mehrere Argumente in eine Funktion aufgenommen werden, die viele Argumente enthält, was zu einer neuen Funktion führt, die die verbleibenden Argumente aufnimmt und ein Ergebnis zurückgibt.

halverist Ihre neue (Curry-) Funktion (oder Schließung), die jetzt nur einen Parameter übernimmt. Ein Anruf halver(10)würde zu 5 führen.

Dafür transformiert es eine Funktion mit n Argumenten in eine Funktion mit n-1 Argumenten. Dasselbe gilt für Ihr Haskell-Beispiel für Curry.

matcauthon
quelle
4
Die Definition von IBM ist falsch. Was sie als Currying definieren, ist eine teilweise Funktionsanwendung, die Argumente einer Funktion bindet (fixiert), um eine Funktion mit kleinerer Arität zu erstellen. Currying transformiert eine Funktion, die mehrere Argumente akzeptiert, in eine Funktionskette, die jeweils ein Argument akzeptiert.
Jordan Gray
1
In der Definition von Wikipedia ist es dasselbe wie in IBM: In Mathematik und Informatik ist das Currying die Technik, eine Funktion, die mehrere Argumente (oder ein n-Tupel von Argumenten) aufnimmt, so zu transformieren, dass sie als a bezeichnet werden kann Funktionskette mit jeweils einem Argument (Teilanwendung). Groovy transformiert eine Funktion (mit zwei Argumenten) mit der rcurryFunktion (die ein Argument annimmt) in eine Funktion (mit nur einem Argument). Ich habe die Curry-Funktion mit einem Argument an meine Basisfunktion angekettet, um meine resultierende Funktion zu erhalten.
matcauthon
3
Nein, die Wikipedia-Definition ist anders - die Teilanwendung ist, wenn Sie die Curry-Funktion aufrufen - nicht, wenn Sie sie definieren, was Groovy macht
jk.
6
@jk ist richtig. Wenn Sie die Wikipedia-Erklärung noch einmal lesen, werden Sie feststellen, dass es sich bei dem zurückgegebenen Wert um eine Funktionskette mit jeweils einem Argument handelt, nicht um eine Funktion mit n - 1Argumenten. Siehe das Beispiel am Ende meiner Antwort; Weitere Informationen zur Unterscheidung finden Sie später in diesem Artikel. en.wikipedia.org/wiki/…
Jordan Gray
4
Es ist sehr wichtig, vertrau mir. Wieder zeigt der Code am Ende meiner Antwort, wie eine korrekte Implementierung funktionieren würde; Zum einen würde es keine Argumente erfordern. Die aktuelle Implementierung sollte eigentlich zB benannt werden partial.
Jordan Gray