Scala currying vs funciones parcialmente aplicadas

82

Me doy cuenta de que hay varias preguntas aquí sobre qué son las funciones de currización y parcialmente aplicadas, pero estoy preguntando en qué se diferencian. Como ejemplo simple, aquí hay una función al curry para encontrar números pares:

def filter(xs: List[Int], p: Int => Boolean): List[Int] =
   if (xs.isEmpty) xs
   else if (p(xs.head)) xs.head :: filter(xs.tail, p)
   else filter(xs.tail, p)

def modN(n: Int)(x: Int) = ((x % n) == 0)

Entonces podrías escribir lo siguiente para usar esto:

val nums = List(1,2,3,4,5,6,7,8)
println(filter(nums, modN(2))

que devuelve: List(2,4,6,8). Pero descubrí que puedo hacer lo mismo de esta manera:

def modN(n: Int, x: Int) = ((x % n) == 0)

val p = modN(2, _: Int)
println(filter(nums, p))

que también devuelve: List(2,4,6,8).

Entonces, mi pregunta es, ¿cuál es la principal diferencia entre los dos y cuándo usaría uno sobre el otro? ¿Es este un ejemplo demasiado simplista para mostrar por qué se usaría uno sobre el otro?

Eric
fuente
Cuando se aplica parcialmente, los costos de representar una versión con curry y sin curry en la memoria pueden ser diferentes, por lo que el rendimiento del tiempo de ejecución también puede verse afectado. (Es decir, si el optimizador no es lo suficientemente inteligente para elegir la representación óptima en ambos casos). Sin embargo, no estoy lo suficientemente familiarizado con Scala para decir cuáles son las diferencias exactas.
1
Eche un vistazo a este: stackoverflow.com/questions/8063325/…
Utaal
Encontré esta explicación muy, muy útil, explica sobre funciones parciales, funciones parcialmente aplicadas y curry, todo en una publicación: stackoverflow.com/a/8650639/1287554
Plasty Grove
Excelente enlace, @PlastyGrove. ¡Gracias!
Eric
Y gracias @Utaal por tu enlace. Cualquier respuesta del propio Martin Odersky es muy valorada. Creo que estos conceptos están comenzando a hacer clic ahora.
Eric

Respuestas:

88

La diferencia semántica se ha explicado bastante bien en la respuesta vinculada por Plasty Grove .

Sin embargo, en términos de funcionalidad, no parece haber mucha diferencia. Veamos algunos ejemplos para verificarlo. Primero, una función normal:

scala> def modN(n: Int, x: Int): Boolean = ((x % n) == 0)
scala> modN(5, _ : Int)
res0: Int => Boolean = <function1>

Entonces obtenemos un parcialmente aplicado <function1>que toma an Int, porque ya le hemos dado el primer número entero. Hasta ahora tan bueno. Ahora al curry:

scala> def modNCurried(n: Int)(x: Int): Boolean = ((x % n) == 0)

Con esta notación, ingenuamente esperaría que funcione lo siguiente:

scala> modNCurried(5)
<console>:9: error: missing arguments for method modN;
follow this method with `_' if you want to treat it as a partially applied function
          modNCurried(5)

Por lo tanto, la notación de lista de parámetros múltiples realmente no parece crear una función curry de inmediato (supuestamente para evitar gastos generales innecesarios), sino que espera a que indique explícitamente que desea curry (la notación también tiene otras ventajas ):

scala> modNCurried(5) _
res24: Int => Boolean = <function1>

Que es exactamente lo mismo que obtuvimos antes, así que no hay diferencia aquí, excepto por la notación. Otro ejemplo:

scala> modN _
res35: (Int, Int) => Boolean = <function2>

scala> modNCurried _
res36: Int => (Int => Boolean) = <function1>

Esto demuestra cómo la aplicación parcial de una función "normal" da como resultado una función que toma todos los parámetros, mientras que la aplicación parcial de una función con múltiples listas de parámetros crea una cadena de funciones, una por lista de parámetros que, todas devuelven una nueva función:

scala> def foo(a:Int, b:Int)(x:Int)(y:Int): Int = a * b + x - y
scala> foo _
res42: (Int, Int) => Int => (Int => Int) = <function2>

scala> res42(5)
<console>:10: error: not enough arguments for method apply: (v1: Int, v2: Int)Int => (Int => Int) in trait Function2.
Unspecified value parameter v2.

Como puede ver, debido a que la primera lista de parámetros de footiene dos parámetros, la primera función en la cadena de curry tiene dos parámetros.


En resumen, las funciones parcialmente aplicadas no son realmente diferentes de las funciones curry en términos de funcionalidad. Esto se verifica fácilmente dado que puede convertir cualquier función a una curry:

scala> (modN _).curried
res45: Int => (Int => Boolean) = <function1

scala> modNCurried _
res46: Int => (Int => Boolean) = <function1>

Post Scriptum

Nota: La razón por la que su ejemplo println(filter(nums, modN(2))funciona sin el subrayado después modN(2)parece ser que el compilador de Scala simplemente asume ese subrayado como una conveniencia para el programador.


Además: como @asflierl ha señalado correctamente, Scala no parece ser capaz de inferir el tipo cuando aplica parcialmente funciones "normales":

scala> modN(5, _)
<console>:9: error: missing parameter type for expanded function ((x$1) => modN(5, x$1))

Considerando que esa información está disponible para funciones escritas usando notación de lista de parámetros múltiples:

scala> modNCurried(5) _
res3: Int => Boolean = <function1>

Esta respuesta muestra cómo esto puede ser muy útil.

fresskoma
fuente
una diferencia importante es que la inferencia de tipos funciona de manera diferente: gist.github.com/4529020
Gracias, agregué una nota con respecto a su comentario :)
fresskoma
19

Curry tiene que ver con tuplas: convertir una función que toma un argumento de tupla en una que toma n argumentos separados, y viceversa . Recordar esto es la clave para distinguir el curry de la aplicación parcial, incluso en idiomas que no admiten el curry.

curry :: ((a, b) -> c) -> a -> b -> c 
   -- curry converts a function that takes all args in a tuple
   -- into one that takes separate arguments

uncurry :: (a -> b -> c) -> (a, b) -> c
   -- uncurry converts a function of separate args into a function on pairs.

La aplicación parcial es la capacidad de aplicar una función a algunos argumentos, produciendo una nueva función para los argumentos restantes .

Es fácil de recordar si solo piensa que curry es la transformación que tiene que ver con las tuplas.

En los idiomas que se currizan de forma predeterminada (como Haskell), la diferencia es clara: tienes que hacer algo para pasar argumentos en una tupla. Pero la mayoría de los otros lenguajes, incluido Scala, no están curvados de forma predeterminada: todos los argumentos se pasan como tuplas, por lo que curry / uncurry es mucho menos útil y menos obvio. Y la gente incluso termina pensando que la aplicación parcial y el curry son lo mismo, ¡solo porque no pueden representar funciones de curry fácilmente!

Don Stewart
fuente
Totalmente de acuerdo. En términos de Scala, "currying" en el significado original de la palabra es el "proceso de transformación" de una función con una lista de parámetros en una función con múltiples listas de parámetros. En Scala esta transformación se puede ejecutar usando ".curried". Desafortunadamente, Scala parece haber sobrecargado un poco el significado de la palabra, ya que originalmente se llamaba ".curry" en lugar de ".curried".
Fatih Coşkun
2

Función multivariable:

def modN(n: Int, x: Int) = ((x % n) == 0)

Curry (o la función de curry):

def modNCurried(n: Int)(x: Int) = ((x % n) == 0)

Por lo tanto, no es una función aplicada parcialmente que sea comparable al curry. Es la función multivariable. Lo que es comparable a la función parcialmente aplicada es el resultado de la invocación de una función curry, que es una función con la misma lista de parámetros que tiene la función parcialmente aplicada.

lcn
fuente
0

Solo para aclarar el último punto.

Además: como @asflierl ha señalado correctamente, Scala no parece ser capaz de inferir el tipo cuando aplica parcialmente funciones "normales":

Scala puede inferir tipos si todos los parámetros son comodines, pero no cuando algunos de ellos están especificados y otros no.

scala> modN(_,_)
res38: (Int, Int) => Boolean = <function2>

scala> modN(1,_)
<console>:13: error: missing parameter type for expanded function ((x$1) => modN(1, x$1))
       modN(1,_)
              ^
Sud
fuente
0

La mejor explicación que pude encontrar hasta ahora: https://dzone.com/articles/difference-between-currying-amp-partially-applied

Currying: descomposición de funciones con múltiples argumentos en una cadena de funciones de un solo argumento. Tenga en cuenta que Scala permite pasar una función como argumento a otra función.

Aplicación parcial de la función: pasar a una función menos argumentos de los que tiene en su declaración. Scala no lanza una excepción cuando proporciona menos argumentos a la función, simplemente los aplica y devuelve una nueva función con el resto de argumentos que deben pasarse.

Danylo Zatorsky
fuente