Jaka jest różnica między wieloma listami parametrów a wieloma parametrami na liście w Scali?

81

W Scali można pisać (curried?) Funkcje takie jak ta

def curriedFunc(arg1: Int) (arg2: String) = { ... }

Jaka jest różnica między powyższą curriedFuncdefinicją funkcji z dwiema listami parametrów a funkcjami z wieloma parametrami na jednej liście parametrów:

def curriedFunc(arg1: Int, arg2: String) = { ... }

Z matematycznego punktu widzenia to jest (curriedFunc(x))(y)i curriedFunc(x,y)ale mogę pisać def sum(x) (y) = x + yi tak samo będziedef sum2(x, y) = x + y

Wiem tylko jedną różnicę - to częściowo zastosowane funkcje. Ale oba sposoby są dla mnie równoważne.

Czy są jakieś inne różnice?

den bardadym
źródło

Odpowiedzi:

88

Ściśle mówiąc, nie jest to funkcja curried, ale metoda z wieloma listami argumentów, chociaż wprawdzie wygląda jak funkcja.

Jak powiedziałeś, listy wielu argumentów pozwalają na użycie metody zamiast częściowo zastosowanej funkcji. (Przepraszam za ogólnie głupie przykłady, których używam)

object NonCurr {
  def tabulate[A](n: Int, fun: Int => A) = IndexedSeq.tabulate(n)(fun)
}

NonCurr.tabulate[Double](10, _)            // not possible
val x = IndexedSeq.tabulate[Double](10) _  // possible. x is Function1 now
x(math.exp(_))                             // complete the application

Inną korzyścią jest to, że zamiast nawiasów można używać nawiasów klamrowych, co wygląda ładnie, jeśli druga lista argumentów składa się z pojedynczej funkcji lub thunk. Na przykład

NonCurr.tabulate(10, { i => val j = util.Random.nextInt(i + 1); i - i % 2 })

przeciw

IndexedSeq.tabulate(10) { i =>
  val j = util.Random.nextInt(i + 1)
  i - i % 2
}

Lub dla thunk:

IndexedSeq.fill(10) {
  println("debug: operating the random number generator")
  util.Random.nextInt(99)
}

Kolejną zaletą jest to, że możesz odwołać się do argumentów z poprzedniej listy argumentów w celu zdefiniowania domyślnych wartości argumentów (chociaż możesz również powiedzieć, że jest to wada, że ​​nie możesz tego zrobić na pojedynczej liście :)

// again I'm not very creative with the example, so forgive me
def doSomething(f: java.io.File)(modDate: Long = f.lastModified) = ???

Wreszcie w odpowiedzi na pokrewny post znajdują się trzy inne aplikacje. Dlaczego Scala udostępnia zarówno listy wielu parametrów, jak i wiele parametrów na listę? . Po prostu skopiuję je tutaj, ale zasługa Knuta Arne Vedaa, Kevina Wrighta i extempore.

Po pierwsze: możesz mieć wiele argumentów var:

def foo(as: Int*)(bs: Int*)(cs: Int*) = as.sum * bs.sum * cs.sum

... co nie byłoby możliwe w przypadku pojedynczej listy argumentów.

Po drugie, wspomaga wnioskowanie o typie:

def foo[T](a: T, b: T)(op: (T,T) => T) = op(a, b)
foo(1, 2){_ + _}   // compiler can infer the type of the op function

def foo2[T](a: T, b: T, op: (T,T) => T) = op(a, b)
foo2(1, 2, _ + _)  // compiler too stupid, unfortunately

I na koniec, jest to jedyny sposób, w jaki możesz mieć niejawne i niejawne argumenty, podobnie jak implicitmodyfikator dla całej listy argumentów:

def gaga [A](x: A)(implicit mf: Manifest[A]) = ???   // ok
def gaga2[A](x: A, implicit mf: Manifest[A]) = ???   // not possible
0__
źródło
2
Ponieważ jest to najczęściej głosowana odpowiedź, myślę, że tytuł pytania nie odpowiada już jego odpowiedzi. Myślę, że tytuł powinien zostać zmieniony na, powiedzmy, „Dlaczego Scala dostarcza zarówno wiele list parametrów, jak i wiele parametrów na listę?”, Tj. Jest już scalony przez próbki z plikiem stackoverflow.com/questions/4684185/… .
Jacek Laskowski
42

Jest jeszcze jedna różnica, która nie została uwzględniona w doskonałej odpowiedzi 0 __ : parametry domyślne. Parametru z jednej listy parametrów można użyć podczas obliczania wartości domyślnej na innej liście parametrów, ale nie na tej samej.

Na przykład:

def f(x: Int, y: Int = x * 2) = x + y // not valid
def g(x: Int)(y: Int = x * 2) = x + y // valid
Daniel C. Sobral
źródło
Wziąłem ten prosty przykład, aby to zrozumieć. To sprawia, że ​​parametry domyślne są znacznie bardziej użyteczne. Dzięki!
Mike McFarland
Dobry przykład, z wyjątkiem tego, że poświęciłem pięć minut, aby dowiedzieć się, jak to nazwać: g(1)()zwraca 3. g(1)(2)zwraca 5.
Sapience
19

O to chodzi, w tym, że curry i nieuruszone formy są równoważne! Jak zauważyli inni, praca z jedną lub drugą formą może być wygodniejsza pod względem składniowym w zależności od sytuacji i jest to jedyny powód, dla którego należy preferować jedną z nich.

Ważne jest, aby zrozumieć, że nawet gdyby Scala nie miała specjalnej składni do deklarowania funkcji curried, nadal można by je skonstruować; jest to po prostu matematyczna nieuchronność, gdy masz już możliwość tworzenia funkcji zwracających funkcje.

Aby to zademonstrować, wyobraź sobie, że def foo(a)(b)(c) = {...}składnia nie istnieje. Następnie można jeszcze osiągnąć dokładnie to samo tak: def foo(a) = (b) => (c) => {...}.

Podobnie jak wiele funkcji w Scali, jest to tylko wygoda składniowa do robienia czegoś, co i tak byłoby możliwe, ale z nieco większą szczegółowością.

Tom Crockett
źródło
4

Te dwie formy są izomorficzne. Główną różnicą jest to, że funkcje curried są łatwiejsze do zastosowania częściowo, podczas gdy funkcje niezwiązane z curry mają nieco ładniejszą składnię, przynajmniej w Scali.

hammar
źródło
2
Czy nie powiedziano wcześniej, że przykłady nie są funkcjami curry? Rozumiem, że funkcja curried ma tylko jeden argument i może zwrócić funkcję z jednym argumentem i tak dalej, aż pojawi się treść z zamkniętymi wszystkimi argumentami. Czy się mylę?
Jacek Laskowski