Zwei Arten, in Scala zu curryen; Was ist der Anwendungsfall für jeden?

83

Ich habe eine Diskussion über mehrere Parameterlisten im Scala Style Guide, den ich pflege. Mir ist klar geworden, dass es zwei Arten des Currying gibt , und ich frage mich, was die Anwendungsfälle sind:

def add(a:Int)(b:Int) = {a + b}
// Works
add(5)(6)
// Doesn't compile
val f = add(5)
// Works
val f = add(5)_
f(10) // yields 15

def add2(a:Int) = { b:Int => a + b }
// Works
add2(5)(6)
// Also works
val f = add2(5)
f(10) // Yields 15
// Doesn't compile
val f = add2(5)_

Der Styleguide impliziert fälschlicherweise, dass diese identisch sind, wenn dies eindeutig nicht der Fall ist. Der Leitfaden versucht, einen Punkt über erstellte Curry-Funktionen zu machen, und obwohl das zweite Formular kein "by-the-Book" -Currying ist, ist es dem ersten Formular immer noch sehr ähnlich (obwohl es wohl einfacher zu verwenden ist, weil Sie es nicht benötigen die _)

Was ist bei denjenigen, die diese Formulare verwenden, der Konsens darüber, wann ein Formular über dem anderen verwendet werden soll?

davetron5000
quelle

Antworten:

135

Methoden mit mehreren Parameterlisten

Für Typinferenz

Methoden mit mehreren Parameterabschnitten können verwendet werden, um die lokale Typinferenz zu unterstützen, indem Parameter im ersten Abschnitt verwendet werden, um Typargumente abzuleiten, die im nachfolgenden Abschnitt einen erwarteten Typ für ein Argument bereitstellen. foldLeftin der Standardbibliothek ist das kanonische Beispiel dafür.

def foldLeft[B](z: B)(op: (B, A) => B): B

List("").foldLeft(0)(_ + _.length)

Wenn dies so geschrieben wäre als:

def foldLeft[B](z: B, op: (B, A) => B): B

Man müsste explizitere Typen angeben:

List("").foldLeft(0, (b: Int, a: String) => a + b.length)
List("").foldLeft[Int](0, _ + _.length)

Für fließende API

Eine andere Verwendung für Methoden mit mehreren Parameterabschnitten besteht darin, eine API zu erstellen, die wie ein Sprachkonstrukt aussieht. Der Anrufer kann anstelle von Klammern geschweifte Klammern verwenden.

def loop[A](n: Int)(body: => A): Unit = (0 until n) foreach (n => body)

loop(2) {
   println("hello!")
}

Die Anwendung von N Argumentlisten auf Methoden mit M Parameterabschnitten, wobei N <M ist, kann explizit mit einem _oder implizit mit einem erwarteten Typ von in eine Funktion konvertiert werden FunctionN[..]. Dies ist eine Sicherheitsfunktion. Hintergrundinformationen finden Sie in den Änderungshinweisen für Scala 2.0 in den Scala-Referenzen.

Curry-Funktionen

Curry-Funktionen (oder einfach Funktionen, die Funktionen zurückgeben) lassen sich leichter auf N Argumentlisten anwenden.

val f = (a: Int) => (b: Int) => (c: Int) => a + b + c
val g = f(1)(2)

Diese kleine Bequemlichkeit lohnt sich manchmal. Beachten Sie, dass Funktionen nicht typparametrisch sein können. In einigen Fällen ist daher eine Methode erforderlich.

Ihr zweites Beispiel ist ein Hybrid: eine Ein-Parameter-Abschnittsmethode, die eine Funktion zurückgibt.

Mehrstufige Berechnung

Wo sonst sind Curry-Funktionen nützlich? Hier ist ein Muster, das ständig auftaucht:

def v(t: Double, k: Double): Double = {
   // expensive computation based only on t
   val ft = f(t)

   g(ft, k)
}

v(1, 1); v(1, 2);

Wie können wir das Ergebnis teilen f(t)? Eine übliche Lösung besteht darin, eine vektorisierte Version von v:

def v(t: Double, ks: Seq[Double]: Seq[Double] = {
   val ft = f(t)
   ks map {k => g(ft, k)}
}

Hässlich! Wir haben nicht verwandte Bedenken verwickelt - Berechnung g(f(t), k)und Abbildung über eine Folge von ks.

val v = { (t: Double) =>
   val ft = f(t)
   (k: Double) => g(ft, k)       
}
val t = 1
val ks = Seq(1, 2)
val vs = ks map (v(t))

Wir könnten auch eine Methode verwenden, die eine Funktion zurückgibt. In diesem Fall ist es etwas besser lesbar:

def v(t:Double): Double => Double = {
   val ft = f(t)
   (k: Double) => g(ft, k)       
}

Wenn wir jedoch versuchen, dasselbe mit einer Methode mit mehreren Parameterabschnitten zu tun, bleiben wir stecken:

def v(t: Double)(k: Double): Double = {
                ^
                `-- Can't insert computation here!
}
Retronym
quelle
3
Tolle Antworten; Ich wünschte, ich hätte mehr positive Stimmen als nur eine. Ich werde verdauen und mich beim Styleguide bewerben. Wenn ich erfolgreich bin, ist dies die gewählte Antwort…
davetron5000
1
Vielleicht möchten Sie Ihr Schleifenbeispiel korrigieren, um:def loop[A](n: Int)(body: => A): Unit = (0 until n) foreach (n => body)
Omer van Kloeten
Dies kompiliert nicht:val f: (a: Int) => (b: Int) => (c: Int) = a + b + c
Nilskp
"Die Anwendung von N Argumentlisten auf Methoden mit M Parameterabschnitten, wobei N <M ist, kann explizit mit einem _ oder implizit mit einem erwarteten Funktionstyp [..] in eine Funktion konvertiert werden." <br/> Sollte es nicht FunctionX [..] sein, wo X = MN?
Stackoverflower
"Dies wird nicht kompiliert: val f: (a: Int) => (b: Int) => (c: Int) = a + b + c" Ich glaube nicht "f: (a: Int) = > (b: Int) => (c: Int) "ist die richtige Syntax. Wahrscheinlich bedeutete Retronym "f: Int => Int => Int => Int". Da => richtig assoziativ ist, ist dies tatsächlich "f: Int => (Int => (Int => Int))". Also ist f (1) (2) vom Typ Int => Int (
dh
16

Sie können nur Funktionen curry, keine Methoden. addist eine Methode, daher müssen Sie die _Konvertierung in eine Funktion erzwingen. add2gibt eine Funktion zurück, so dass dies _nicht nur unnötig ist, sondern hier keinen Sinn ergibt.

In Anbetracht der unterschiedlichen Methoden und Funktionen (z. B. aus Sicht der JVM) macht Scala in den meisten Fällen einen ziemlich guten Job, indem sie die Grenze zwischen ihnen verwischt und "The Right Thing" ausführt, aber es gibt einen Unterschied, und manchmal müssen Sie nur darüber Bescheid wissen.

Landei
quelle
Das macht Sinn. Wie nennt man dann das Formular def add (a: Int) (b: Int)? Was ist der Begriff / die Phrase, die den Unterschied zwischen diesem Def und Def Add beschreibt (a: Int, b: Int)?
davetron5000
@ davetron5000 Die erste ist eine Methode mit mehreren Parameterlisten, die zweite eine Methode mit einer Parameterliste.
Jesper
5

Ich denke, es hilft, die Unterschiede zu erfassen, wenn ich hinzufüge, dass def add(a: Int)(b: Int): IntSie bei Ihnen so ziemlich nur eine Methode mit zwei Parametern definieren, nur diese beiden Parameter werden in zwei Parameterlisten gruppiert (siehe die Konsequenzen davon in anderen Kommentaren). Tatsächlich int add(int a, int a)betrifft diese Methode Java (nicht Scala!). Wenn Sie schreiben add(5)_, ist das nur ein Funktionsliteral, eine kürzere Form von { b: Int => add(1)(b) }. Auf der anderen Seite add2(a: Int) = { b: Int => a + b }definieren Sie mit Ihnen eine Methode, die nur einen Parameter hat, und für Java wird es sein scala.Function add2(int a). Wenn Sie add2(1)in Scala schreiben , ist dies nur ein einfacher Methodenaufruf (im Gegensatz zu einem Funktionsliteral).

Beachten Sie auch, dass dies add(möglicherweise) weniger Overhead verursacht als add2wenn Sie sofort alle Parameter angeben. Wie add(5)(6)nur add(5, 6)auf der JVM-Ebene übersetzt, wird kein FunctionObjekt erstellt. Auf der anderen Seite add2(5)(6)wird zuerst ein FunctionObjekt erstellt, das einschließt 5, und dann dieses aufgerufen apply(6).

ddekany
quelle