Dos formas de curry en Scala; ¿Cuál es el caso de uso de cada uno?

83

Estoy teniendo una discusión sobre las listas de parámetros múltiples en la Guía de estilo de Scala que mantengo. Me he dado cuenta de que hay dos formas de curry , y me pregunto cuáles son los casos de uso:

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

La guía de estilo insinúa incorrectamente que son iguales, cuando claramente no lo son. La guía está tratando de hacer un punto sobre las funciones creadas con curry y, si bien la segunda forma no es curry "según el libro", sigue siendo muy similar a la primera forma (aunque podría decirse que es más fácil de usar porque no necesita el _)

De aquellos que usan estos formularios, ¿cuál es el consenso sobre cuándo usar un formulario sobre el otro?

davetron5000
fuente

Respuestas:

137

Métodos de lista de parámetros múltiples

Para la inferencia de tipo

Los métodos con múltiples secciones de parámetros se pueden usar para ayudar a la inferencia de tipos locales, usando parámetros en la primera sección para inferir argumentos de tipo que proporcionarán un tipo esperado para un argumento en la sección siguiente. foldLeften la biblioteca estándar es el ejemplo canónico de esto.

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

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

Si esto estuviera escrito como:

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

Habría que proporcionar tipos más explícitos:

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

Para API fluida

Otro uso de los métodos de sección de parámetros múltiples es crear una API que parezca una construcción de lenguaje. La persona que llama puede usar llaves en lugar de paréntesis.

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

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

La aplicación de N listas de argumentos a métodos con M secciones de parámetros, donde N <M, se puede convertir a una función explícitamente con a _, o implícitamente, con un tipo esperado de FunctionN[..]. Esta es una característica de seguridad, vea las notas de cambio para Scala 2.0, en las Referencias de Scala, para un fondo.

Funciones al curry

Las funciones curry (o simplemente, las funciones que devuelven funciones) se pueden aplicar más fácilmente a N listas de argumentos.

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

Esta pequeña conveniencia a veces vale la pena. Sin embargo, tenga en cuenta que las funciones no pueden ser de tipo paramétrico, por lo que en algunos casos se requiere un método.

Su segundo ejemplo es un híbrido: un método de sección de un parámetro que devuelve una función.

Computación de múltiples etapas

¿Dónde más son útiles las funciones de curry? Aquí hay un patrón que aparece todo el tiempo:

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

¿Cómo podemos compartir el resultado f(t)? Una solución común es proporcionar una versión vectorizada de v:

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

¡Feo! Hemos enredado preocupaciones no relacionadas: calcular g(f(t), k)y mapear una secuencia de 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))

También podríamos usar un método que devuelva una función. En este caso, es un poco más legible:

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

Pero si intentamos hacer lo mismo con un método con múltiples secciones de parámetros, nos quedamos atascados:

def v(t: Double)(k: Double): Double = {
                ^
                `-- Can't insert computation here!
}
retrónimo
fuente
3
Grandes respuestas; Ojalá tuviera más votos a favor que solo uno. Voy a digerir y aplicar a la guía de estilo; si tengo éxito, estas son las respuestas elegidas ...
davetron5000
1
Es posible que desee corregir su ejemplo de bucle a:def loop[A](n: Int)(body: => A): Unit = (0 until n) foreach (n => body)
Omer van Kloeten
Esto no compila:val f: (a: Int) => (b: Int) => (c: Int) = a + b + c
nilskp
"La aplicación de N listas de argumentos a métodos con M secciones de parámetros, donde N <M, se puede convertir a una función explícitamente con un _, o implícitamente, con un tipo esperado de FunctionN [..]". <br/> ¿No debería ser FunctionX [..], donde X = MN?
stackoverflower
"Esto no compila: val f: (a: Int) => (b: Int) => (c: Int) = a + b + c" No creo "f: (a: Int) = > (b: Int) => (c: Int) "es la sintaxis correcta. Probablemente el retrónimo significaba "f: Int => Int => Int => Int". Debido a que => es asociativo a la derecha, en realidad es "f: Int => (Int => (Int => Int))". Entonces, f (1) (2) es de tipo Int => Int (es decir, el bit más interno en el tipo de f)
stackoverflower
16

Puede curry solo funciones, no métodos. addes un método, por lo que necesita _para forzar su conversión a una función. add2devuelve una función, por lo _que no solo es innecesario, sino que no tiene sentido aquí.

Teniendo en cuenta cómo los diferentes métodos y funciones son (por ejemplo, desde la perspectiva de la JVM), Scala hace un trabajo bastante bueno borrando la línea entre ellos y haciendo "lo correcto" en la mayoría de los casos, pero no es una diferencia, y a veces sólo tiene para saberlo.

Landei
fuente
Esto tiene sentido, entonces, ¿cómo se llama a la forma def add (a: Int) (b: Int) entonces? ¿Cuál es el término / frase que describe la diferencia entre def y def add (a: Int, b: Int)?
davetron5000
@ davetron5000 el primero es un método con múltiples listas de parámetros, el segundo es un método con una lista de parámetros.
Jesper
5

Creo que ayuda a comprender las diferencias si agrego que con def add(a: Int)(b: Int): Intusted prácticamente solo defina un método con dos parámetros, solo esos dos parámetros se agrupan en dos listas de parámetros (vea las consecuencias de eso en otros comentarios). De hecho, ese método es en lo int add(int a, int a)que respecta a Java (¡no a Scala!). Cuando escribe add(5)_, es solo una función literal, una forma más corta de { b: Int => add(1)(b) }. Por otro lado, con add2(a: Int) = { b: Int => a + b }usted define un método que tiene solo un parámetro, y para Java lo será scala.Function add2(int a). Cuando escribe add2(1)en Scala, es solo una llamada a un método simple (a diferencia de una función literal).

También tenga en cuenta que addtiene (potencialmente) menos gastos generales que add2si proporciona inmediatamente todos los parámetros. Como add(5)(6)simplemente se traduce enadd(5, 6) en el nivel de JVM, no Functionse crea ningún objeto. Por otro lado, add2(5)(6)primero creará un Functionobjeto que encierra 5, y luego lo invocará apply(6).

ddekany
fuente