Два способи каррі в Scala; який варіант використання для кожного?


83

Я обговорюю декілька списків параметрів у керівництві Scala Style, яке я веду . Я зрозумів, що є два способи каррі , і мені цікаво, якими є варіанти використання:

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

У посібнику зі стилю неправильно випливає, що вони однакові, коли явно ні. Посібник намагається вказати на створені функціональні функції, і, хоча друга форма не є карріруванням, що зберігається за книгою, вона все ще дуже схожа на першу форму (хоча, можливо, простіша у використанні, оскільки вам не _)

Який консенсус щодо тих, хто використовує ці форми, щодо того, коли використовувати одну форму над іншою?

Відповіді:


137

Кілька методів списку параметрів

Для виведення типу

Методи з декількома розділами параметрів можуть бути використані для сприяння виводу локального типу, використовуючи параметри в першому розділі для виведення аргументів типу, які нададуть очікуваний тип для аргументу в наступному розділі. foldLeftу стандартній бібліотеці - канонічний приклад цього.

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

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

Якби це було написано так:

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

Треба було б надати більш явні типи:

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

Для вільного API

Іншим використанням методів з декількома розділами параметрів є створення API, який виглядає як конструкція мови. Абонент може використовувати фігурні дужки замість дужок.

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

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

Застосування N списків аргументів до методу з розділами параметрів M, де N <M, може бути перетворено у функцію явно з a _або неявно з очікуваним типом FunctionN[..]. Це функція безпеки, дивіться попередні відомості щодо змін до Scala 2.0 у Довідниках Scala.

Функції каррі

Карировані функції (або просто функції, які повертають функції) легше застосовувати до N списків аргументів.

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

Ця незначна зручність часом варта. Зверніть увагу, що функції не можуть бути параметричними за типом, тому в деяких випадках потрібен метод.

Другим вашим прикладом є гібрид: метод розділу з одним параметром, який повертає функцію.

Багатоетапне обчислення

Де ще корисні функції, що використовуються в каррі? Ось шаблон, який постійно з’являється:

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

Як ми можемо поділитися результатом f(t)? Загальним рішенням є надання векторизованої версії v:

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

Некрасиво! Ми переплутали непов’язані проблеми - обчислення g(f(t), k)та відображення послідовності 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))

Ми також могли б використовувати метод, який повертає функцію. У цьому випадку це трохи читабельніше:

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

Але якщо ми спробуємо зробити те саме з методом з декількома розділами параметрів, ми застрягли:

def v(t: Double)(k: Double): Double = {
                ^
                `-- Can't insert computation here!
}

3
Чудові відповіді; хоч би я мав більше голосів, ніж лише один. Я збираюся переварити і застосувати до керівництва зі стилю; якщо я успішно, це обрані відповіді ...
davetron5000

1
Можливо, ви захочете виправити приклад свого циклу на:def loop[A](n: Int)(body: => A): Unit = (0 until n) foreach (n => body)
Омер ван Клоєтен

Це не компілюється:val f: (a: Int) => (b: Int) => (c: Int) = a + b + c
nilskp

"Застосування N списків аргументів до методу з розділами параметрів M, де N <M, може бути перетворено у функцію явно з _, або неявно, з очікуваним типом Функції N [..]." <br/> Хіба це не має бути функціяX [..], де X = MN?
stackoverflower

"Це не компілюється: val f: (a: Int) => (b: Int) => (c: Int) = a + b + c" Я не думаю "f: (a: Int) = > (b: Int) => (c: Int) "- правильний синтаксис. Можливо, ретронім означав "f: Int => Int => Int => Int". Оскільки => є правильною асоціацією, це насправді "f: Int => (Int => (Int => Int))". Отже, f (1) (2) має тип Int => Int (тобто внутрішній самий біт типу f)
stackoverflower

16

Ви можете каррі лише функції, а не методи. addє методом, тому вам потрібно _примусити його перетворити на функцію. add2повертає функцію, тому _тут не тільки непотрібно, але й немає сенсу.

З огляду на те, як різні методи і функції (наприклад , з точки зору віртуальної машини Java), Scala робить дуже хорошу роботу стираючи грань між ними і робити «правильно» в більшості випадків, але є різниця, а іноді просто необхідна щоб про це знати.


Це має сенс, то як тоді ви називаєте форму def add (a: Int) (b: Int)? Який термін / фраза описує різницю між цим def та def add (a: Int, b: Int)?
davetron5000

@ davetron5000 перший - це метод із декількома списками параметрів, другий - метод із одним списком параметрів.
Jesper

5

Я думаю, що це допомагає зрозуміти відмінності, якщо додати, що з def add(a: Int)(b: Int): Intвами майже просто визначте метод з двома параметрами, лише ці два параметри згруповані у два списки параметрів (див. Наслідки цього в інших коментарях). Насправді цей метод стосується лише int add(int a, int a)Java (а не Scala!). Коли ви пишете add(5)_, це просто літерал функції, коротша форма { b: Int => add(1)(b) }. З іншого боку, add2(a: Int) = { b: Int => a + b }ви визначаєте метод, який має лише один параметр, і для Java це буде scala.Function add2(int a). Коли ви пишете add2(1)в Scala, це просто звичайний виклик методу (на відміну від літералу функції).

Також зверніть увагу, що addнакладні витрати (потенційно) менші, ніж add2, якщо ви негайно надасте всі параметри. Як і add(5)(6)просто переклад add(5, 6)на рівні JVM, жоден Functionоб’єкт не створюється. З іншого боку, add2(5)(6)спочатку створить Functionоб'єкт, який закриває 5, а потім викликає його apply(6).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.