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

83

Я веду обсуждение списков нескольких параметров в Руководстве по стилю Scala, которое я поддерживаю. Я понял, что есть два способа каррирования , и мне интересно, каковы варианты использования:

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

В руководстве по стилю неправильно подразумевается, что это одно и то же, хотя это явно не так. В руководстве делается акцент на созданных каррированных функциях, и, хотя вторая форма не является каррированием «по книге», она по-прежнему очень похожа на первую форму (хотя, возможно, ее проще использовать, потому что вам не нужно в _)

Каково мнение тех, кто использует эти формы, относительно того, когда использовать одну форму вместо другой?

davetron5000
источник

Ответы:

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, может быть преобразовано в функцию явно с _или неявно с ожидаемым типом 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, может быть преобразовано в функцию явно с _ или неявно с ожидаемым типом FunctionN [..]». <br/> Разве это не должно быть FunctionX [..], где 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 делает очень хорошую работу стирая грань между ними и делать «правильно» в большинстве случаев, но есть разница, а иногда просто необходима знать об этом.

Landei
источник
В этом есть смысл, так как же тогда назвать форму def add (a: Int) (b: Int)? Какой термин / фраза описывает разницу между def и def add (a: Int, b: Int)?
davetron5000 06
@ davetron5000 первый - это метод с несколькими списками параметров, второй - метод с одним списком параметров.
Джеспер
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).

ддеканы
источник