Dwa sposoby curry w Scali; jaki jest przypadek użycia dla każdego?

83

Prowadzę dyskusję na temat list wielu parametrów w przewodniku po stylu Scala, który prowadzę. Zdałem sobie sprawę, że istnieją dwa sposoby curry i zastanawiam się, jakie są przypadki użycia:

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

Przewodnik po stylu błędnie sugeruje, że są one takie same, podczas gdy wyraźnie nie są. Przewodnik próbuje poruszyć kwestię utworzonych funkcji curried i chociaż druga forma nie jest curry z książki, nadal jest bardzo podobna do pierwszej (choć prawdopodobnie łatwiejsza w użyciu, ponieważ the_ )

Jaki jest konsensus co do tego, kiedy używają tych formularzy, kiedy należy używać jednego formularza zamiast drugiego?

davetron5000
źródło

Odpowiedzi:

137

Wiele metod listy parametrów

Dla wnioskowania o typie

Metody z wieloma sekcjami parametrów mogą służyć do wspomagania wnioskowania o typie lokalnym, używając parametrów w pierwszej sekcji do wywnioskowania argumentów typu, które zapewnią oczekiwany typ argumentu w kolejnej sekcji. foldLeftw bibliotece standardowej jest to kanoniczny przykład.

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

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

Gdyby to było zapisane jako:

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

Należałoby podać bardziej wyraźne typy:

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

Dla płynnego API

Innym zastosowaniem metod sekcji z wieloma parametrami jest utworzenie interfejsu API, który wygląda jak konstrukcja językowa. Osoba wywołująca może użyć nawiasów klamrowych zamiast nawiasów.

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

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

Zastosowanie list N argumentów do metody z sekcjami parametrów M, gdzie N <M, można przekonwertować na funkcję jawnie za pomocą _lub niejawnie z oczekiwanym typem FunctionN[..]. Jest to funkcja bezpieczeństwa, zapoznaj się z uwagami o zmianach dla Scala 2.0, w Referencjach Scala, aby zapoznać się z tłem.

Funkcje Curried

Funkcje curried (lub po prostu funkcje, które zwracają funkcje) można łatwiej zastosować do list N argumentów.

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

Ta niewielka wygoda czasami się opłaca. Zauważ, że funkcje nie mogą być parametryczne, więc w niektórych przypadkach wymagana jest metoda.

Twój drugi przykład to hybryda: jednoparametrowa metoda sekcji, która zwraca funkcję.

Obliczenia wieloetapowe

Gdzie jeszcze przydatne są funkcje curry? Oto wzór, który pojawia się cały czas:

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

Jak możemy podzielić się wynikiem f(t)? Typowym rozwiązaniem jest zapewnienie zwektoryzowanej wersji v:

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

Brzydki! Poplątaliśmy niepowiązane problemy - obliczanie g(f(t), k)i mapowanie w sekwencji 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))

Moglibyśmy również użyć metody, która zwraca funkcję. W tym przypadku jest nieco bardziej czytelny:

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

Ale jeśli spróbujemy zrobić to samo z metodą z wieloma sekcjami parametrów, utkniemy:

def v(t: Double)(k: Double): Double = {
                ^
                `-- Can't insert computation here!
}
retronim
źródło
3
Świetne odpowiedzi; Chciałbym mieć więcej głosów za niż tylko jeden. Mam zamiar przetrawić i zastosować do przewodnika po stylu; jeśli mi się uda, to są wybrane odpowiedzi…
davetron5000,
1
Możesz skorygować przykład pętli do:def loop[A](n: Int)(body: => A): Unit = (0 until n) foreach (n => body)
Omer van Kloeten
To się nie kompiluje:val f: (a: Int) => (b: Int) => (c: Int) = a + b + c
nilskp
„Zastosowanie list N argumentów do metody z sekcjami M parametrów, gdzie N <M, można przekonwertować na funkcję jawnie z _ lub niejawnie z oczekiwanym typem FunctionN [..]”. <br/> Czy nie powinna to być funkcja X [..], gdzie X = MN?
stackoverflower
"To się nie kompiluje: val f: (a: Int) => (b: Int) => (c: Int) = a + b + c" Nie sądzę "f: (a: Int) = > (b: Int) => (c: Int) ”to właściwa składnia. Prawdopodobnie retronim oznaczał „f: Int => Int => Int => Int”. Ponieważ => jest prawostronnie asocjacyjne, to w rzeczywistości jest to "f: Int => (Int => (Int => Int))". Czyli f (1) (2) jest typu Int => Int (czyli najbardziej wewnętrznym bitem typu f)
stackoverflower
16

Możesz curry tylko funkcji, a nie metod. addjest metodą, więc musisz _wymusić jej konwersję na funkcję. add2zwraca funkcję, więc _jest nie tylko niepotrzebna, ale nie ma tu sensu.

Biorąc pod uwagę, jak różne są metody i funkcje (np. Z punktu widzenia JVM), Scala całkiem nieźle radzi sobie z zatarciem granicy między nimi i w większości przypadków robi „The Right Thing”, ale jest różnica, a czasami wystarczy wiedzieć o tym.

Landei
źródło
To ma sens, więc jak nazywasz formę def add (a: Int) (b: Int)? Jaki jest termin / fraza opisująca różnicę między tym def i def add (a: Int, b: Int)?
davetron5000
@ davetron5000 pierwsza to metoda z wieloma listami parametrów, druga to metoda z jedną listą parametrów.
Jesper
5

Myślę, że pomocne jest uchwycenie różnic, jeśli dodam, że def add(a: Int)(b: Int): Intpo prostu zdefiniuj metodę z dwoma parametrami, tylko te dwa parametry są zgrupowane na dwóch listach parametrów (zobacz konsekwencje tego w innych komentarzach). W rzeczywistości ta metoda jest tak int add(int a, int a)samo ważna jak Java (nie Scala!). Kiedy piszesz add(5)_, jest to po prostu literał funkcji, krótsza forma { b: Int => add(1)(b) }. Z drugiej strony, wraz z add2(a: Int) = { b: Int => a + b }tobą definiujesz metodę, która ma tylko jeden parametr, a dla Javy będzie nim scala.Function add2(int a). Kiedy piszesz add2(1)w Scali, jest to zwykłe wywołanie metody (w przeciwieństwie do literału funkcji).

Zauważ również, że addma (potencjalnie) mniej narzutu niż add2ma, jeśli natychmiast podasz wszystkie parametry. Tak jak add(5)(6)po prostu przekłada się add(5, 6)na poziomie JVM, żaden Functionobiekt nie jest tworzony. Z drugiej strony add2(5)(6)najpierw utworzy Functionobiekt, który otacza 5, a następnie wywoła apply(6)to.

ddekany
źródło