Saltar al contenido

Explicando una implementación de currying en Haskell

Este team de especialistas pasados ciertos días de trabajo y recopilación de de información, obtuvimos la respuesta, nuestro deseo es que resulte de utilidad en tu trabajo.

Solución:

Podemos comenzar una variable a la vez:

curry f a b = f (a, b)

a y b son valores no restringidos (además de ser argumentos a la f función), lo que significa que podemos darles tipos, digamos a y b (naming muy creativo, lo sé).

Entonces, la firma actual se ve así (notación abusiva):

curry :: (type of f) -> a -> b -> (type of f (a, b))

Ya que f se llama (a, b) en el lado derecho, podemos asumir que es una función que toma una tupla (a, b) y devuelve algún valor, digamos c. Entonces, f es (a, b) -> c, asi que f (a, b) es un nivel aplicado, o simplemente c. Entonces, la firma de la función es:

curry :: ((a, b) -> c) -> a -> b -> c

Ahora, podemos intentar darle sentido a esta firma. Me resulta más fácil de entender cuando está entre paréntesis como esta expresión equivalente:

curry :: ((a, b) -> c) -> (a -> b -> c)

Pasas una función que toma una tupla y devuelve un valor, y curry devuelve una función que toma dos valores y devuelve un valor. Esencialmente, está desempaquetando los argumentos: pasando de una función que toma una sola tupla que contiene 2 valores, a una función que toma los 2 valores.

Mirando esto en el contexto de curry fst, fst es (a, b) -> a. Entonces, curry descompondría los argumentos, haciéndolo a b -> a. Esta es básicamente la definición de const, devolviendo el primer argumento e ignorando el segundo.

Ahora podemos mirar uncurry. Usando el mismo razonamiento que antes, podemos decir que el argumento a tiene tipo a, y el argumento b tiene tipo b, y desde f se llama a luego b, debe tener algún tipo a -> b -> c. Entonces, la firma de tipo se ve así:

uncurry :: (a -> b -> c) -> (a, b) -> c

O, entre paréntesis como antes:

uncurry :: (a -> b -> c) -> ((a, b) -> c)

Como sugiere el nombre, esto es lo opuesto al curry. Toma una función que ya toma 2 argumentos, luego devuelve una función que toma una tupla con los dos argumentos. Mirando a uncurry en un caso de uso:

λ> f = uncurry (+)
λ> :t f
f :: Num c => (c, c) -> c
λ> f (1, 2)
3
λ> (+) 1 2
3

También, curry (uncurry f) debiera ser f, para cualquier f que incluye dos o más argumentos.

Quizás esto sea demasiado obvio para señalarlo, pero para muchas funciones de Haskell que operan en otras funciones, es más fácil entenderlas (o de hecho, construirlas) tomando las definiciones como equivalencias directas o reescribiendo reglas, en lugar de tratar de averiguar cómo están escritos o cómo “operan internamente”.

Por ejemplo, si tengo un uncurry función definida por:

uncurry f (a, b) = f a b

y quiere entender cómo funciona, solo tome un ejemplo de una función curry:

times a b = a * b

y aplicar uncurry a eso:

uncurry times

La definición anterior implica que esta expresión resultante se puede aplicar a un par y reescribir:

(uncurry times) (4, 8) = times 4 8

A partir de esto, debe quedar muy claro cómo se da cualquier función al curry f que se aplica a dos argumentos curry, la expresión uncurry f producirá una versión sin cursar que se aplica en su lugar a un par. ¿Qué podría ser más sencillo? No queda nada por “entender”.

Y si quiero implementar una función en lugar de entender una implementación dada? Por ejemplo, si tengo una función no curiosa, como add (a,b) = a + b, supongamos que quiero currylo. Bueno, si tuviera el curry función para hacerlo:

curry add

entonces, ¿cómo debería comportarse? La función curry add tomaría dos argumentos al curry:

(curry add) a b

y su resultado sería la aplicación de un curry add a una tupla que contiene esos argumentos:

(curry add) a b = add (a,b)

Si generalizo esto:

(curry f) a b = f (a,b)

Obtengo exactamente la declaración de nivel superior que define curry. Puede usarlo tal como está y se compilará, aunque es más habitual eliminar los paréntesis redundantes de la izquierda.

Esta es una forma útil de escribir implementaciones de otras funciones de alto nivel sin pensarlo mucho. ¿Cómo debería redactar el operador . ¿estar definido? Bueno, si compuse dos funciones f . g, su aplicación a un argumento debería ser la composición:

(f . g) x = f (g x)

Esta es una definición de nivel superior perfectamente válida de .. Por razones de rendimiento (es decir, afecta la inserción), en realidad se implementa en Prelude como:

(.) f g = x -> f (g x)

pero la definición anterior basada en reescritura todavía funciona.

Otras funciones de alto nivel (a veces aparentemente complicadas) tienen definiciones basadas en reescritura igualmente sencillas, aunque a veces es difícil detectar lo que se está definiendo:

flip f x y = f y x
x & f = f x

-- define "on" from Data.Function, which applies a binary operation
-- to f-transformed arguments
(binop `on` f) x y = binop (f x) (f y)

Tenga en cuenta que curry también podría ser escrito

curry f = a -> b -> f (a, b)

Porque curry en sí está curry, solo se necesita un argumento, la función de tipo (t1, t2) -> t. La definición curry f a b = ... en sí mismo es, en cierto sentido, azúcar sintáctico para esta expresión lambda definida explícitamente.

Recuerda que tienes autorización de decir si te fue útil.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *