haskell - languages - Typed FP: Tuple Arguments y Curriable Arguments
functional programming python (5)
En los lenguajes de programación funcional de tipo estático, como Standard ML, F #, OCaml y Haskell, una función generalmente se escribirá con los parámetros separados el uno del otro y del nombre de la función simplemente por el espacio en blanco:
let add a b =
a + b
El tipo aquí es " int -> (int -> int)
", es decir, una función que toma un int y devuelve una función que toma su turno y int y que finalmente devuelve un int. Por lo tanto, el currying se hace posible.
También es posible definir una función similar que tome una tupla como argumento:
let add(a, b) =
a + b
El tipo se convierte en " (int * int) -> int
" en este caso.
Desde el punto de vista del diseño del lenguaje, ¿hay alguna razón por la que uno no podría simplemente identificar estos dos patrones de tipo en el tipo de álgebra? En otras palabras, de modo que "(a * b) -> c" se reduce a "a -> (b -> c)", permitiendo que ambas variantes se curried con la misma facilidad.
Supongo que esta pregunta debe haber surgido cuando se diseñaron idiomas como los cuatro que mencioné. Entonces, ¿alguien sabe alguna razón o investigación que indique por qué los cuatro de estos idiomas eligieron no "unificar" estos dos patrones de tipo?
Al menos una razón para no confundir los tipos funcionales currificados y no ejecutados es que sería muy confuso cuando las tuplas se usan como valores devueltos (una forma conveniente en estos lenguajes escritos para devolver múltiples valores). En el sistema de tipo combinado, ¿cómo puede funcionar la función siendo muy composable? ¿A a -> (a * a)
también se transformaría en a a -> a -> a
? Si es así, ¿son (a * a) -> a
y a -> (a * a)
del mismo tipo? Si no, ¿cómo redactaría a -> (a * a)
con (a * a) -> a
?
Usted propone una solución interesante para el problema de la composición. Sin embargo, no me parece satisfactorio porque no encaja bien con la aplicación parcial, que es realmente una conveniencia clave de las funciones al curry. Permítanme intentar ilustrar el problema en Haskell:
apply f a b = f a b
vecSum (a1,a2) (b1,b2) = (a1+b1,a2+b2)
Ahora, tal vez su solución podría entender el map (vecSum (1,1)) [(0,1)]
, pero ¿qué pasa con el map (apply vecSum (1,1)) [(0,1)]
equivalente map (apply vecSum (1,1)) [(0,1)]
? ¡Se vuelve complicado! ¿Su desempaquetado completo significa que el (1,1) está desempaquetado con los argumentos a & b de apply? Esa no es la intención ... y en cualquier caso, el razonamiento se vuelve complicado.
En resumen, creo que sería muy difícil combinar las funciones currículum y no curvo mientras (1) se conserva la semántica del código válido bajo el sistema antiguo y (2) se proporciona una intuición y algoritmo razonables para el sistema fusionado. Es un problema interesante, sin embargo.
Ampliando los comentarios bajo la buena respuesta de namin:
Asuma una función que tenga el tipo ''a -> (''a * ''a)
:
let gimme_tuple(a : int) =
(a*2, a*3)
Luego asuma una función que tenga tipo (''a * ''a) -> ''b
:
let add(a : int, b) =
a + b
Entonces la composición (asumiendo la fusión que propongo) no supondría ningún problema por lo que puedo ver:
let foo = add(gimme_tuple(5))
// foo gets value 5*2 + 5*3 = 25
Pero entonces podría concebir una función polimórfica que tome el lugar de add
en el último fragmento de código, por ejemplo, una pequeña función que solo toma una 2-tupla y hace una lista de los dos elementos:
let gimme_list(a, b) =
[a, b]
Esto tendría el tipo (''a * ''a) -> (''a list)
. La composición ahora sería problemática. Considerar:
let bar = gimme_list(gimme_tuple(5))
bar
tendría ahora el valor [10, 15] : int list
o bar
sería una función de tipo (int * int) -> ((int * int) list)
, que eventualmente devolvería una lista cuya cabeza sería la tupla (10, 15)
? Para que esto funcione, postulé en un comentario a la respuesta de Namin que necesitaría una regla adicional en el sistema de tipo que la vinculación de los parámetros reales con los formales sea la "más completa posible", es decir, que el sistema intente un enlace no parcial primero y solo pruebe un enlace parcial si no se puede obtener un enlace completo. En nuestro ejemplo, significaría que obtenemos el valor [10, 15]
porque es posible un enlace completo en este caso.
¿Es ese concepto de "lo más completo posible" inherentemente sin sentido? No lo sé, pero no puedo ver de inmediato una razón por la que sería.
Un problema es, por supuesto, si quieres la segunda interpretación del último fragmento de código, entonces necesitarías saltar un aro adicional (normalmente una función anónima) para obtenerlo.
Creo que el consenso de hoy es manejar múltiples argumentos currying (la a -> b -> c
) y reservar tuplas para cuando realmente quieres valores de tipo tupla (en listas, etc.). Este consenso es respetado por cada lenguaje funcional tipado estáticamente desde Standard ML, que (puramente como una cuestión de convención) utiliza tuplas para funciones que toman múltiples argumentos.
¿Por qué esto es tan? El estándar ML es el más antiguo de estos lenguajes, y cuando las personas escribían compiladores ML por primera vez, no se sabía cómo manejar los argumentos curried de manera eficiente. (En la raíz del problema está el hecho de que cualquier función curried podría ser parcialmente aplicada por algún otro código que no haya visto aún, y debe compilarlo teniendo en cuenta esa posibilidad). Desde que se diseñó ML estándar, los compiladores tienen mejorado, y puede leer sobre el estado del arte en un excelente artículo de Simon Marlow y Simon Peyton Jones .
LISP, que es el lenguaje funcional más antiguo de todos ellos, tiene una sintaxis concreta que es extremadamente hostil al currying y la aplicación parcial. Hrmph.
Las tuplas no están presentes en estos lenguajes simplemente para usarse como parámetros de función. Son una forma muy conveniente de representar datos estructurados, por ejemplo, un punto 2D ( int * int
), un elemento de lista ( ''a * ''a list
), o un nodo de árbol ( ''a * ''a tree * ''a tree
) . Recuerde también que las estructuras son solo azúcar sintáctico para las tuplas. Es decir,
type info =
{ name : string;
age : int;
address : string; }
es una forma conveniente de manejar una tupla (string * int * string)
.
No hay una razón fundamental para que necesite tuplas en un lenguaje de programación (puede construir tuplas en el cálculo lambda, del mismo modo que puede booleanos y enteros, de hecho, usando currying *), pero son convenientes y útiles.
*
Por ejemplo,
tuple a b = λf.f a b
fst x = x (λa.λb.a)
snd x = x (λa.λb.b)
curry f = λa.λb.f (λg.g a b)
uncurry f = λx.x f
Posiblemente porque es útil tener una tupla como un tipo separado, y es bueno mantener diferentes tipos separados?
En Haskell al menos, es bastante fácil pasar de una forma a otra:
Prelude> let add1 a b = a+b
Prelude> let add2 (a,b) = a+b
Prelude> :t (uncurry add1)
(uncurry add1) :: (Num a) => (a, a) -> a
Prelude> :t (curry add2)
(curry add2) :: (Num a) => a -> a -> a
así que no se uncurry add1
es lo mismo que add2
y curry add2
es lo mismo que add1
. Creo que cosas similares son posibles en los otros idiomas también. ¿Qué mayores ganancias habría para currying automáticamente cada función definida en una tupla? (Dado que eso es lo que pareces estar preguntando)