¿Cuál es la ventaja de hacer curry?

147

Acabo de aprender sobre el curry y, si bien creo que entiendo el concepto, no veo ninguna gran ventaja en su uso.

Como ejemplo trivial, uso una función que agrega dos valores (escritos en ML). La versión sin curry sería

fun add(x, y) = x + y

y se llamaría como

add(3, 5)

mientras que la versión al curry es

fun add x y = x + y 
(* short for val add = fn x => fn y=> x + y *)

y se llamaría como

add 3 5

Me parece que es solo un azúcar sintáctico que elimina un conjunto de paréntesis para definir y llamar a la función. He visto el curry como una de las características importantes de los lenguajes funcionales, y me siento un poco decepcionado por el momento. El concepto de crear una cadena de funciones que consumen cada uno un solo parámetro, en lugar de una función que toma una tupla, parece bastante complicado de usar para un simple cambio de sintaxis.

¿Es la sintaxis un poco más simple la única motivación para hacer curry, o me estoy perdiendo otras ventajas que no son obvias en mi ejemplo tan simple? ¿El curry es solo azúcar sintáctica?

    
pregunta Mad Scientist 01.02.2013 - 20:36

15 respuestas

123

Con las funciones al curry, es más fácil reutilizar las funciones más abstractas, ya que se especializa. Digamos que tiene una función de adición

add x y = x + y

y que desea agregar 2 a cada miembro de una lista. En Haskell harías esto:

map (add 2) [1, 2, 3] -- gives [3, 4, 5]
-- actually one could just do: map (2+) [1, 2, 3], but that may be Haskell specific

Aquí la sintaxis es más clara que si tuvieras que crear una función add2

add2 y = add 2 y
map add2 [1, 2, 3]

o si tuvieras que hacer una función lambda anónima:

map (\y -> 2 + y) [1, 2, 3]

También le permite abstraerse de diferentes implementaciones. Digamos que tenías dos funciones de búsqueda. Uno de una lista de pares clave / valor y una clave para un valor y otro de un mapa de claves a valores y una clave para un valor como este:

lookup1 :: [(Key, Value)] -> Key -> Value -- or perhaps it should be Maybe Value
lookup2 :: Map Key Value -> Key -> Value

Entonces podrías hacer una función que aceptara una función de búsqueda de Clave a Valor. Puede pasarlo a cualquiera de las funciones de búsqueda anteriores, aplicadas parcialmente con una lista o un mapa, respectivamente:

myFunc :: (Key -> Value) -> .....

En conclusión: el curry es bueno, porque le permite especializar / aplicar parcialmente las funciones usando una sintaxis liviana y luego pasar estas funciones parcialmente aplicadas a una función de orden superior como map o filter . Las funciones de orden superior (que toman las funciones como parámetros o las obtienen como resultados) son el pan y la mantequilla de la programación funcional, y las funciones de curry y parcialmente aplicadas permiten que las funciones de orden superior se usen de manera mucho más eficaz y concisa.

    
respondido por el Boris 01.02.2013 - 21:23
52

La respuesta práctica es que el curry facilita mucho la creación de funciones anónimas. Incluso con una sintaxis lambda mínima, es algo así como una victoria; comparar:

map (add 1) [1..10]
map (\ x -> add 1 x) [1..10]

Si tienes una sintaxis lambda fea, es aún peor. (Te estoy mirando, JavaScript, Scheme y Python).

Esto se vuelve cada vez más útil a medida que usa más y más funciones de orden superior. Mientras uso más funciones de orden superior en Haskell que en otros idiomas, descubrí que en realidad uso la sintaxis lambda menos porque algo así como dos tercios de las veces, el Lambda sería solo una función parcialmente aplicada. (Y gran parte de la otra vez lo extraigo en una función nombrada).

Más fundamentalmente, no siempre es obvio qué versión de una función es "canónica". Por ejemplo, tome map . El tipo de map se puede escribir de dos maneras:

map :: (a -> b) -> [a] -> [b]
map :: (a -> b) -> ([a] -> [b])

¿Cuál es el "correcto"? En realidad es difícil de decir. En la práctica, la mayoría de los idiomas usan el primero: el mapa toma una función y una lista y devuelve una lista. Sin embargo, fundamentalmente, lo que realmente hace el mapa es asignar las funciones normales a las funciones de lista: toma una función y devuelve una función. Si el mapa está en curry, no tiene que responder esta pregunta: hace ambos , de una manera muy elegante.

Esto se vuelve especialmente importante una vez que generalice map a tipos distintos a la lista.

Además, el curry realmente no es muy complicado. En realidad, es una pequeña simplificación sobre el modelo que utilizan la mayoría de los idiomas: no necesita ninguna noción de funciones de múltiples argumentos incorporados en su idioma. Esto también refleja más de cerca el cálculo lambda subyacente.

Por supuesto, los lenguajes de estilo ML no tienen una noción de múltiples argumentos en forma de curry o sin formato. La sintaxis f(a, b, c) en realidad corresponde a pasar la tupla (a, b, c) a f , por lo que f aún toma el argumento. Esta es en realidad una distinción muy útil que desearía que tuvieran otros idiomas porque hace que sea muy natural escribir algo como:

map f [(1,2,3), (4,5,6), (7, 8, 9)]

¡No podrías hacer esto fácilmente con idiomas que tienen la idea de múltiples argumentos incorporados!

    
respondido por el Tikhon Jelvis 01.02.2013 - 21:31
21

El curry puede ser útil si tiene una función que está pasando como un objeto de primera clase y no recibe todos los parámetros necesarios para evaluarla en un lugar en el código. Simplemente puede aplicar uno o más parámetros cuando los obtenga y pase el resultado a otra pieza de código que tenga más parámetros y termine de evaluarlos allí.

El código para lograr esto será más simple que si primero necesita reunir todos los parámetros.

También, existe la posibilidad de que se reutilice más el código, ya que las funciones que toman un solo parámetro (otra función al curry) no tienen que coincidir como específicamente con todos los parámetros.

    
respondido por el psr 01.02.2013 - 21:07
14

La motivación principal (al menos inicialmente) para hacer curry no fue práctica sino teórica. En particular, el curry le permite obtener eficazmente funciones de múltiples argumentos sin definir realmente la semántica para ellos o la semántica para los productos. Esto conduce a un lenguaje más simple con tanta expresividad como otro lenguaje más complicado, y por lo tanto es deseable.

    
respondido por el Alex R 02.02.2013 - 15:12
14

(Daré ejemplos en Haskell.)

  1. Cuando se utilizan lenguajes funcionales, es muy conveniente que pueda aplicar una función parcialmente. Como en Haskell, (== x) es una función que devuelve True si su argumento es igual a un término dado x :

    mem :: Eq a => a -> [a] -> Bool
    mem x lst = any (== x) lst
    

    sin curry, tendríamos un código un poco menos legible:

    mem x lst = any (\y -> y == x) lst
    
  2. Esto está relacionado con la programación tácita (consulte también estilo Pointfree en el wiki de Haskell). Este estilo no se centra en los valores representados por variables, sino en la composición de funciones y en cómo la información fluye a través de una cadena de funciones. Podemos convertir nuestro ejemplo en un formulario que no usa variables en absoluto:

    mem = any . (==)
    

    Aquí vemos == como una función de a a a -> Bool y any como una función de a -> Bool a [a] -> Bool . Al simplemente componerlos, obtenemos el resultado. Todo esto es gracias al curry.

  3. El reverso, un-curry, también es útil en algunas situaciones. Por ejemplo, digamos que queremos dividir una lista en dos partes: elementos que son más pequeños que 10 y el resto, y luego concatenar esas dos listas. La división de la lista se realiza mediante partition (< 10) (aquí también usamos curry < ). El resultado es de tipo ([Int],[Int]) . En lugar de extraer el resultado en su primera y segunda parte y combinarlos con ++ , podemos hacerlo directamente eliminando ++ as

    uncurry (++) . partition (< 10)
    

De hecho, (uncurry (++) . partition (< 10)) [4,12,11,1] se evalúa como [4,1,12,11] .

También hay importantes ventajas teóricas:

  1. El curry es esencial para los idiomas que carecen de tipos de datos y tienen solo funciones, como el cálculo lambda . Si bien estos idiomas no son útiles para el uso práctico, son muy importantes desde un punto de vista teórico.
  2. Esto está conectado con la propiedad esencial de los lenguajes funcionales: las funciones son objetos de primera clase. Como hemos visto, la conversión de (a, b) -> c a a -> (b -> c) significa que el resultado de la última función es del tipo b -> c . En otras palabras, el resultado es una función.
  3. (Un) currying está estrechamente relacionado con categorías cerradas cartesianas , que es una forma categórica de ver los cálculos lambda mecanografiados.
respondido por el Petr Pudlák 01.02.2013 - 22:14
9

¡El curry no es solo azúcar sintáctica!

Considere las firmas de tipo de add1 (sin apurar) y add2 (al curry):

add1 : (int * int) -> int
add2 : int -> (int -> int)

(En ambos casos, los paréntesis en la firma de tipo son opcionales, pero los he incluido para mayor claridad)

add1 es una función que toma una tupla de int y int y devuelve un int . add2 es una función que toma un int y devuelve otra función que a su vez toma un int y devuelve un int .

La diferencia esencial entre los dos se hace más visible cuando especificamos explícitamente la aplicación de la función. Definamos una función (no al curry) que aplique su primer argumento a su segundo argumento:

apply(f, b) = f b

Ahora podemos ver la diferencia entre add1 y add2 más claramente. add1 se llama con un 2-tuple:

apply(add1, (3, 5))

pero se llama a add2 con un int y luego se llama a su valor de retorno con otro int :

apply(apply(add2, 3), 5)

EDITAR: El beneficio esencial de hacer currying es que obtienes una aplicación parcial de forma gratuita. Digamos que quería una función de tipo int -> int (por ejemplo, a map it en una lista) que agregó 5 a su parámetro. Podría escribir addFiveToParam x = x+5 , o podría hacer el equivalente con un lambda en línea, pero también podría hacerlo mucho más fácilmente (especialmente en casos menos triviales que este) ¡escriba add2 5 !

    
respondido por el Ptharien's Flame 01.02.2013 - 20:51
9

Currying es solo azúcar sintáctica, pero creo que no entiendes lo que hace el azúcar. Tomando tu ejemplo,

fun add x y = x + y

es en realidad azúcar sintáctica para

fun add x = fn y => x + y

Es decir, (agregar x) devuelve una función que toma un argumento y, y agrega x a y.

fun addTuple (x, y) = x + y

Esa es una función que toma una tupla y agrega sus elementos. Esas dos funciones son en realidad bastante diferentes; Toman argumentos diferentes.

Si desea agregar 2 a todos los números en una lista:

(* add 2 to all numbers using the uncurried function *)
map (fn x => addTuple (x, 2)) [1,2,3]
(* using the curried function *)
map (add 2) [1,2,3]

El resultado sería [3,4,5] .

Por otra parte, si desea sumar cada tupla en una lista, la función addTuple se ajusta perfectamente.

(* Sum each tuple using the uncurried function *)
map addTuple [(10,2), (10,3), (10,4)]    
(* sum each tuple using curried function *)
map (fn (a,b) => add a b) [(10,2), (10,3), (10,4)]

El resultado sería [12,13,14] .

Las funciones al curry son excelentes cuando la aplicación parcial es útil, por ejemplo, map, fold, app, filter. Considere esta función, que devuelve el mayor número positivo en la lista suministrada, o 0 si no hay números positivos:

- val highestPositive = foldr Int.max 0;   
val highestPositive = fn : int list -> int 
    
respondido por el gnud 02.02.2013 - 15:43
9

Otra cosa que no he visto mencionada todavía es que el curry permite la abstracción (limitada) sobre la aridad.

Considere estas funciones que forman parte de la biblioteca de Haskell

(.) :: (b -> c) -> (a -> b) -> a -> c
either :: (a -> c) -> (b -> c) -> Either a b -> c
flip :: (a -> b -> c) -> b -> a -> c
on :: (b -> b -> c) -> (a -> b) -> a -> a -> c

En cada caso, la variable de tipo c puede ser un tipo de función para que estas funciones funcionen en algún prefijo de la lista de parámetros de su argumento. Sin curry, necesitaría una función de lenguaje especial para abstraerse sobre la función arity o tener muchas versiones diferentes de estas funciones especializadas para diferentes arities.

    
respondido por el Geoff Reedy 03.02.2013 - 04:01
6

Mi comprensión limitada es tal:

1) Aplicación de función parcial

Aplicación de función parcial es el proceso de devolver una función que toma un número menor de argumentos. Si proporciona 2 de 3 argumentos, devolverá una función que toma 3-2 = 1 argumento. Si proporciona 1 de 3 argumentos, devolverá una función que toma 3-1 = 2 argumentos. Si quisiera, incluso podría aplicar parcialmente 3 de los 3 argumentos y devolvería una función que no acepta ningún argumento.

Dada la siguiente función:

f(x,y,z) = x + y + z;

Al vincular 1 a xy aplicarlo parcialmente a la función anterior f(x,y,z) obtendrías:

f(1,y,z) = f'(y,z);

Donde: f'(y,z) = 1 + y + z;

Ahora, si tuviera que vincular y a 2 y z a 3, y aplicar parcialmente f'(y,z) , obtendría:

f'(2,3) = f''();

Donde: f''() = 1 + 2 + 3 ;

Ahora, en cualquier punto, puede elegir evaluar f , f' o f'' . Así que puedo hacer:

print(f''()) // and it would return 6;

o

print(f'(1,1)) // and it would return 3;

2) Currying

Currying , por otro lado, es el proceso de dividir una función en una cadena anidada de un argumento. Nunca puedes proporcionar más de 1 argumento, es uno o cero.

Dada la misma función:

f(x,y,z) = x + y + z;

Si lo hicieras, obtendrías una cadena de 3 funciones:

f'(x) -> f''(y) -> f'''(z)

Donde:

f'(x) = x + f''(y);

f''(y) = y + f'''(z);

f'''(z) = z;

Ahora si llama a f'(x) con x = 1 :

f'(1) = 1 + f''(y);

Se te devuelve una nueva función:

g(y) = 1 + f''(y);

Si llama a g(y) con y = 2 :

g(2) = 1 + 2 + f'''(z);

Se te devuelve una nueva función:

h(z) = 1 + 2 + f'''(z);

Finalmente, si llama a h(z) con z = 3 :

h(3) = 1 + 2 + 3;

Te devuelven 6 .

3) Cierre

Finalmente, Cierre es el proceso de capturar una función y datos juntos como una sola unidad. El cierre de una función puede llevar de 0 a un número infinito de argumentos, pero también es consciente de los datos que no se le pasan.

De nuevo, dada la misma función:

f(x,y,z) = x + y + z;

En su lugar, puede escribir un cierre:

f(x) = x + f'(y, z);

Donde:

f'(y,z) = x + y + z;

f' está cerrado en x . Lo que significa que f' puede leer el valor de x que está dentro de f .

Entonces, si tuvieras que llamar a f con x = 1 :

f(1) = 1 + f'(y, z);

Obtendría un cierre:

closureOfF(y, z) =
                   var x = 1;
                   f'(y, z);

Ahora, si llamó a closureOfF con y = 2 y z = 3 :

closureOfF(2, 3) = 
                   var x = 1;
                   x + 2 + 3;

Lo que devolvería 6

Conclusión

El curry, la aplicación parcial y los cierres son todos algo similares en cuanto a que descomponen una función en más partes.

El curry descompone una función de múltiples argumentos en funciones anidadas de argumentos únicos que devuelven funciones de argumentos individuales. No tiene sentido aplicar una función de uno o menos argumentos, ya que no tiene sentido.

La aplicación parcial descompone una función de múltiples argumentos en una función de argumentos menores cuyos argumentos ahora perdidos fueron sustituidos por el valor proporcionado.

El cierre descompone una función en una función y un conjunto de datos en el que las variables dentro de la función que no se pasaron pueden mirar dentro del conjunto de datos para encontrar un valor al que vincularse cuando se le pide evaluar.

Lo que es confuso acerca de todo esto es que pueden usarse para implementar un subconjunto de los otros. Así que en esencia, todos son un poco de un detalle de implementación. Todos proporcionan un valor similar en el sentido de que no es necesario recopilar todos los valores por adelantado y en que puede reutilizar parte de la función, ya que la ha descompuesto en unidades discretas.

Disclosure

De ninguna manera soy un experto en el tema, solo recientemente comencé a aprender sobre esto, por lo que ofrezco mi comprensión actual, pero podría tener errores que los invito a señalar y los corregiré. Como / si descubro alguna.

    
respondido por el Didier A. 16.04.2015 - 07:44
5

Currying (aplicación parcial) le permite crear una nueva función a partir de una función existente mediante la fijación de algunos parámetros. Es un caso especial de cierre léxico en el que la función anónima es solo una envoltura trivial que pasa algunos argumentos capturados a otra función. También podemos hacer esto usando la sintaxis general para hacer cierres léxicos, pero la aplicación parcial proporciona un azúcar sintáctico simplificado.

Es por eso que los programadores de Lisp, cuando trabajan en un estilo funcional, a veces usan bibliotecas para aplicaciones parciales .

En lugar de (lambda (x) (+ 3 x)) , que nos da una función que agrega 3 a su argumento, puede escribir algo como (op + 3) , y así agregar 3 a cada elemento de una lista, entonces sería (mapcar (op + 3) some-list) en lugar de %código%. Esta macro (mapcar (lambda (x) (+ 3 x)) some-list) te hará una función que toma algunos argumentos op e invoca x y z ... .

En muchos lenguajes puramente funcionales, la aplicación parcial está incorporada en la sintaxis para que no haya un operador (+ a x y z ...) . Para activar una aplicación parcial, simplemente llame a una función con menos argumentos de los que requiere. En lugar de producir un error op , el resultado es una función de los argumentos restantes.

    
respondido por el Kaz 01.02.2013 - 22:48
4

Para la función

fun add(x, y) = x + y

Es de la forma f': 'a * 'b -> 'c

Para evaluar uno hará

add(3, 5)
val it = 8 : int

Para la función al curry

fun add x y = x + y

Para evaluar uno hará

add 3
val it = fn : int -> int

Donde se trata de un cálculo parcial, específicamente (3 + y), con el que se puede completar el cálculo con

it 5
val it = 8 : int

agregar en el segundo caso es de la forma f: 'a -> 'b -> 'c

Lo que está haciendo el curry aquí es transformar una función que toma dos acuerdos en uno que solo hace que uno devuelva un resultado. Evaluación parcial

  

¿Por qué uno necesita esto?

Decir x en el RHS no es solo un int regular, sino un cómputo complejo que toma un tiempo completar, por ejemplo, aumentos, dos segundos.

x = twoSecondsComputation(z)

Así que la función ahora se ve como

fun add (z:int) (y:int) : int =
    let
        val x = twoSecondsComputation(z)
    in
        x + y
    end;

De tipo add : int * int -> int

Ahora queremos calcular esta función para un rango de números, asignémosla

val result1 = map (fn x => add (20, x)) [3, 5, 7];

Para lo anterior, el resultado de twoSecondsComputation se evalúa cada vez. Esto significa que toma 6 segundos para este cálculo.

El uso de una combinación de puesta en escena y curry puede evitar esto.

fun add (z:int) : int -> int =
    let
        val x = twoSecondsComputation(z)
    in
        (fn y => x + y)
    end;

De la forma curried add : int -> int -> int

Ahora se puede hacer,

val add' = add 20;
val result2 = map add' [3, 5, 7, 11, 13];

El twoSecondsComputation solo necesita evaluarse una vez. Para aumentar la escala, reemplace dos segundos por 15 minutos, o cualquier hora, luego tenga un mapa contra 100 números.

Resumen : el curry es excelente cuando se utiliza con otros métodos para funciones de nivel superior como herramienta de evaluación parcial. Su propósito no puede ser realmente demostrado por sí mismo.

    
respondido por el phwd 02.02.2013 - 16:22
3

El curry permite una composición de función flexible.

Inventé una función "curry". En este contexto, no me importa qué tipo de registrador obtengo o de dónde viene. No me importa qué es la acción o de dónde viene. Todo lo que me importa es procesar mi entrada.

var builder = curry(function(input, logger, action) {
     logger.log("Starting action");
     try {
         action(input);
         logger.log("Success!");
     }
     catch (err) {
         logger.logerror("Boo we failed..", err);
     }
});
var x = "My input.";
goGatherArgs(builder)(x); // Supplies action first, then logger somewhere.

La variable del constructor es una función que devuelve una función que devuelve una función que toma mi entrada que hace mi trabajo. Este es un ejemplo útil simple y no un objeto a la vista.

    
respondido por el mortalapeman 01.02.2013 - 21:53
2

Currying es una ventaja cuando no tienes todos los argumentos para una función. Si está evaluando completamente la función, entonces no hay una diferencia significativa.

Currying le permite evitar mencionar parámetros que aún no son necesarios. Es más conciso y no requiere encontrar un nombre de parámetro que no coincida con otra variable en su alcance (que es mi beneficio favorito).

Por ejemplo, cuando usa funciones que toman funciones como argumentos, a menudo se encontrará en situaciones en las que necesita funciones como "agregar 3 a la entrada" o "comparar entrada con la variable v". Con currying, estas funciones se escriben fácilmente: add 3 y (== v) . Sin currying, tienes que usar expresiones lambda: x => add 3 x y x => x == v . Las expresiones lambda son dos veces más largas y tienen una pequeña cantidad de trabajo ocupado relacionado con la selección de un nombre además de x si ya hay un x en el alcance.

Un beneficio secundario de los idiomas basados en el curry es que, al escribir código genérico para funciones, no se obtienen cientos de variantes basadas en el número de parámetros. Por ejemplo, en C #, un método 'curry' necesitaría variantes para Func < R & gt ;, Func < A, R & gt ;, Func < A1, A2, R & gt ;, Func < A1, A2, A3, R & gt ;, etc. Siempre. En Haskell, el equivalente de un Func < A1, A2, R > es más como un Func < Tuple < A1, A2 & gt ;, R > o un Func < A1, Func < A2, R > > (y un Func < R > es más como un Func < Unit, R >), por lo que todas las variantes corresponden a la única Func < A, R > caso.

    
respondido por el Craig Gidney 01.02.2013 - 23:39
2

El razonamiento principal en el que puedo pensar (y no soy un experto en este tema por ningún medio) comienza a mostrar sus beneficios a medida que las funciones pasan de ser triviales a no triviales. En todos los casos triviales con la mayoría de los conceptos de esta naturaleza, no encontrará ningún beneficio real. Sin embargo, la mayoría de los lenguajes funcionales hacen un uso intensivo de la pila en las operaciones de procesamiento. Considere PostScript o Lisp como ejemplos de esto. Al hacer uso del curry, las funciones se pueden apilar más eficazmente y este beneficio se hace evidente a medida que las operaciones se vuelven cada vez menos triviales. De la forma en curry, el comando y los argumentos se pueden lanzar en la pila en orden y se pueden saltar según sea necesario para que se ejecuten en el orden correcto.

    
respondido por el Peter Mortensen 09.11.2013 - 11:13
0

El curry depende de manera crucial (definitivamente uniforme) de la capacidad de devolver una función.

Considere este pseudo código (ideado).

var f = (m, x, b) = > ... devolver algo ...

Estipulemos que llamar a f con menos de tres argumentos devuelve una función.

var g = f (0, 1); // esto devuelve una función vinculada a 0 y 1 (m y x) que acepta un argumento más (b).

var y = g (42); // invoca g con el tercer argumento faltante, utilizando 0 y 1 para m y x

El hecho de que pueda aplicar parcialmente los argumentos y recuperar una función reutilizable (vinculada a los argumentos que proporcionó) es bastante útil (y SECO).

    
respondido por el Rick O'Shea 06.07.2014 - 18:27

Lea otras preguntas en las etiquetas