tuplas - ¿Qué está pasando cuando compongo*con+en Haskell?
peta haskell en español (5)
Estoy tratando de entender el resultado de
(*) . (+)
en Haskell. Sé que el operador de composición es solo la composición estándar de las funciones matemáticas, por lo que
(f . g) = f (g x)
Pero:
(*) . (+) :: (Num (a -> a), Num a) => a -> (a -> a) -> a -> a
Estoy luchando por entender esta firma de tipo. Hubiera esperado poder hacer cosas como:
((*) . (+)) 1 2 :: Num a => a -> a
= (* (+ 1 2))
Cuál es el significado de (*) . (+) ¿tipo de firma? Intenté jugar con él por algo así como (solo coincidiendo con su firma):
((*) . (+)) 1 (/x -> x + 1) 1
Pero eso no se puede compilar. Estoy tratando de seguir los pasos lógicos al componer estos, pero no entiendo completamente cómo está llegando a este resultado (y cuál es el resultado).
Algunas extensiones primero:
{-# LANGUAGE FlexibleContexts, FlexibleInstances, TypeSynonymInstances #-}
Como muestran las otras respuestas, tu función es
weird :: (Num (a -> a), Num a) => a -> (a -> a) -> a -> a
weird x g = (x +) * g
Pero esta función tiene una semántica no rara.
Hay una noción de listas de diferencias . En consecuencia, hay una noción de enteros de diferencia. He visto que se usan solo en la configuración dependiente de tipeo (por ejemplo, here , pero ese no es el único caso). La parte relevante de la definición es
instance Enum DiffInt where
toEnum n = (n +)
fromEnum n = n 0
instance Num DiffInt where
n + m = n . m
n * m = foldr (+) id $ replicate (fromEnum n) m
Esto no tiene mucho sentido en Haskell, pero puede ser útil con tipos dependientes.
Ahora podemos escribir
test :: DiffInt
test = toEnum 3 * toEnum 4
O
test :: DiffInt
test = weird 3 (toEnum 4)
En ambos casos fromEnum test == 12
.
EDITAR
Es posible evitar el uso de la extensión TypeSynonymInstances
:
{-# LANGUAGE FlexibleContexts, FlexibleInstances #-}
weird :: (Num (a -> a), Num a) => a -> (a -> a) -> a -> a
weird x g = (x +) * g
instance (Enum a, Num a) => Enum (a -> a) where
toEnum n = (toEnum n +)
fromEnum n = fromEnum $ n (toEnum 0)
instance (Enum a, Num a) => Num (a -> a) where
n + m = n . m
n * m = foldr (+) id $ replicate (fromEnum n) m
type DiffInt = Int -> Int
Como antes, podemos escribir
test'' :: DiffInt
test'' = weird 3 (toEnum 4)
Pero ahora también podemos escribir
-- difference ints over difference ints
type DiffDiffInt = DiffInt -> DiffInt
test'''' :: DiffDiffInt
test'''' = weird (toEnum 3) (toEnum (toEnum 4))
Y
main = print $ fromEnum $ fromEnum test''
impresiones 12
.
EDIT2 Se han agregado mejores enlaces.
Aquí hay buenas respuestas, pero permítanme señalar rápidamente algunos pasos en los que salió mal.
Primero, la definición correcta de composición de funciones es
(f . g) x = f (g x)
omitiste la x
en el LHS. Luego, debes recordar que en Haskell hxy
es lo mismo que (hx) y
. Entonces, al contrario de lo que esperabas,
((*) . (+)) 1 2 = (((*) . (+)) 1) 2 = ((*) ((+) 1)) 2 = ((+) 1) * 2,
y ahora ves por qué eso falla. También,
((*) . (+)) 1 (/x -> x + 1) 1
no funciona, porque la restricción Num (Int -> Int)
no se cumple.
Dejar:
m = (*)
a = (+)
entonces
(m.a) x = (m (a x)) = m (a x)
Ahora m
espera un Num a
como parámetro, por otro lado (ax)
, es decir, (x +)
es una función unaria (a -> a)
por definición de (+)
. Supongo que lo que sucedió es que GHC intenta unir estos dos tipos para que, si tiene un tipo que es tanto un número como una función unaria, m
puede tomar un número y una función unaria y devolver una función unaria, ya que se consideran el mismo tipo
Como señaló @Syd, esta unificación no tendría sentido para ningún tipo normal de números como enteros y números de coma flotante.
Entiendo como te sientes. También encontré que la composición de la función es bastante difícil de entender al principio. Lo que me ayudó a asimilar el asunto fueron las firmas de tipo. Considerar:
(*) :: Num x => x -> x -> x
(+) :: Num y => y -> y -> y
(.) :: (b -> c) -> (a -> b) -> a -> c
Ahora cuando escribes (*) . (+)
(*) . (+)
en realidad es lo mismo que (.) (*) (+)
(es decir, (*)
es el primer argumento para (.)
y (+)
es el segundo argumento para (.)
):
(.) :: (b -> c) -> (a -> b) -> a -> c
|______| |______|
| |
(*) (+)
Por lo tanto, la firma de tipo de (*)
(es decir, Num x => x -> x -> x
) se unifica con b -> c
:
(*) :: Num x => x -> x -> x -- remember that `x -> x -> x`
| |____| -- is implicitly `x -> (x -> x)`
| |
b -> c
(.) (*) :: (a -> b) -> a -> c
| |
| |‾‾‾‾|
Num x => x x -> x
(.) (*) :: Num x => (a -> x) -> a -> x -> x
Por lo tanto, la firma de tipo de (+)
(es decir, Num y => y -> y -> y
) se unifica con Num x => a -> x
:
(+) :: Num y => y -> y -> y -- remember that `y -> y -> y`
| |____| -- is implicitly `y -> (y -> y)`
| |
Num x => a -> x
(.) (*) (+) :: Num x => a -> x -> x
| | |
| |‾‾‾‾| |‾‾‾‾|
Num y => y y -> y y -> y
(.) (*) (+) :: (Num (y -> y), Num y) => y -> (y -> y) -> y -> y
Espero que aclare de dónde vienen Num (y -> y)
y Num y
. Te queda una función muy extraña del tipo (Num (y -> y), Num y) => y -> (y -> y) -> y -> y
.
Lo que lo hace tan extraño es que espera que tanto y
como y -> y
sean instancias de Num
. Es comprensible que y
sea una instancia de Num
, pero ¿cómo y -> y
? Hacer y -> y
una instancia de Num
parece ilógico. Eso no puede ser correcto.
Sin embargo, tiene sentido cuando nos fijamos en qué función realmente hace la composición:
( f . g ) = /z -> f ( g z)
((*) . (+)) = /z -> (*) ((+) z)
Entonces tienes una función /z -> (*) ((+) z)
. Por lo tanto, z
debe ser claramente una instancia de Num
porque (+)
se le aplica. Por lo tanto, el tipo de /z -> (*) ((+) z)
es Num t => t -> ...
donde ...
es el tipo de (*) ((+) z)
, que encontraremos fuera en un momento.
Por lo tanto, ((+) z)
es del tipo Num t => t -> t
porque requiere un número más. Sin embargo, antes de aplicarlo a otro número, se le aplica (*)
.
Por lo tanto, (*)
espera que ((+) z)
sea una instancia de Num
, por lo que se espera que t -> t
sea una instancia de Num
. Por lo tanto, ...
se reemplaza por (t -> t) -> t -> t
y se agrega la restricción Num (t -> t)
, lo que da como resultado el tipo (Num (t -> t), Num t) => t -> (t -> t) -> t -> t
.
La forma en que realmente desea combinar (*)
y (+)
es usar (.:)
:
(.:) :: (c -> d) -> (a -> b -> c) -> a -> b -> d
f .: g = /x y -> f (g x y)
Por lo tanto, (*) .: (+)
Es lo mismo que /xy -> (*) ((+) xy)
. Ahora se dan dos argumentos para (+)
asegurar que ((+) xy)
sea de hecho solo Num t => t
y no Num t => t -> t
.
Por lo tanto ((*) .: (+)) 2 3 5
es (*) ((+) 2 3) 5
que es (*) 5 5
que es 25
, que creo que es lo que quiere.
Tenga en cuenta que f .: g
también se puede escribir como (f .) . g
(f .) . g
, y (.:)
también se puede definir como (.:) = (.) . (.)
(.:) = (.) . (.)
. Puedes leer más sobre esto aquí:
Qué hace (f.). g significa en Haskell?
Espero que ayude.
(*)
y (+)
ambos tienen la firma tipo Num a => a -> a -> a
Ahora, si los compones, obtienes algo funky.
(*) . (+) :: (Num (a -> a), Num a) => a -> (a -> a) -> a -> a
Eso es porque (*)
y (+)
esperan dos ''argumentos''.
(+) con un argumento te da una función. El .
el operador espera esa función (la a -> a
que ve).
Aquí está el significado de (*) . (+)
(*) . (+)
x f y
(*) . (+) :: (Num (a -> a), Num a) => a -> (a -> a) -> a -> a
(*) . (+)
(*) . (+)
asigna xfy
a ((x +) * f) y
donde f
es una función de a
a a
que también es un número. La razón (*)
espera que una función sea hacer que los tipos coincidan mientras espera dos argumentos, pero esa función tiene que ser un número porque (*)
solo funciona en números.
Realmente, esta función no tiene ningún sentido.