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

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

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

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

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

Ответы

Ответ 1

Несколько методов списка параметров

Для вывода типа

Методы с несколькими разделами параметров могут использоваться для поддержки локального ввода типа, используя параметры в первом разделе для вывода аргументов типа, которые будут предоставлять ожидаемый тип аргумента в следующем разделе. 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, можно явно преобразовать в функцию с помощью < <25 > или неявно с ожидаемым типом 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!
}

Ответ 2

Вы можете выполнять только функции, а не методы. add - это метод, поэтому вам нужно _ заставить его преобразовать функцию. add2 возвращает функцию, поэтому _ не только не нужен, но и здесь нет смысла.

Учитывая, как различные методы и функции (например, с точки зрения JVM), Scala делает довольно хорошую работу, размывая линию между ними и делая "Правую вещь" в большинстве случаев, но там , и иногда вам просто нужно знать об этом.

Ответ 3

Я думаю, что это помогает понять различия, если я добавлю, что с 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).