usar tuplas peta pattern opciones librerias importar hacer funciones español ejemplos como haskell functional-programming function-composition

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.