Al dar a la firma de tipo explícita, se impide que GHC de hacer ciertas suposiciones acerca de su código. Voy a mostrar un ejemplo (tomado de esta question):
foo (x:y:_) = x == y
foo [_] = foo []
foo [] = False
Según GHCi, el tipo de esta función es Eq a => [a] -> Bool
, como era de esperar. Sin embargo, si declara foo
con esta firma, obtendrá un error de "variable de tipo ambiguo".
La razón por la cual esta función solo funciona sin una firma de tipo es por la forma en que funciona la comprobación de tipo en GHC. Cuando omite una firma de tipo, se supone que foo
tiene el monotipo [a] -> Bool
para algún tipo fijo a
. Una vez que termine de escribir el grupo de enlace, generalice los tipos. Ahí es donde obtienes el forall a. ...
.
Por otro lado, cuando se declara una firma de tipo polimórfico, que explícitamente que foo
es polimórfico (y por lo tanto el tipo de []
no tiene por qué coincidir con el tipo de primer argumento) y la pluma, se obtiene el tipo ambigua variable.
Ahora, sabiendo esto, vamos a comparar el núcleo:
fib = 0:1:zipWith (+) fib (tail fib)
-----
fib :: forall a. Num a => [a]
[GblId, Arity=1]
fib =
\ (@ a) ($dNum :: Num a) ->
letrec {
fib1 [Occ=LoopBreaker] :: [a]
[LclId]
fib1 =
break<3>()
: @ a
(fromInteger @ a $dNum (__integer 0))
(break<2>()
: @ a
(fromInteger @ a $dNum (__integer 1))
(break<1>()
zipWith
@ a @ a @ a (+ @ a $dNum) fib1 (break<0>() tail @ a fib1))); } in
fib1
Y para el segundo:
fib :: Num a => [a]
fib = 0:1:zipWith (+) fib (tail fib)
-----
Rec {
fib [Occ=LoopBreaker] :: forall a. Num a => [a]
[GblId, Arity=1]
fib =
\ (@ a) ($dNum :: Num a) ->
break<3>()
: @ a
(fromInteger @ a $dNum (__integer 0))
(break<2>()
: @ a
(fromInteger @ a $dNum (__integer 1))
(break<1>()
zipWith
@ a
@ a
@ a
(+ @ a $dNum)
(fib @ a $dNum)
(break<0>() tail @ a (fib @ a $dNum))))
end Rec }
Con la firma de tipo explícita, como con foo
anterior, GHC tiene que tratar fib
como valor potencialmente polimórficamente recursivo. nos podíamos dejar pasar algunos Num
diccionario diferente a fib
en zipWith (+) fib ...
y en este punto habría que tirar la mayor parte de la lista de distancia, desde diferentes Num
medio distinto (+)
. Por supuesto, una vez que compila con optimizaciones, GHC nota que el diccionario Num
nunca cambia durante "llamadas recursivas" y lo optimiza.
En el núcleo de arriba, puede ver que GHC efectivamente da fib
un diccionario Num
(llamado $dNum
) una y otra vez.
Debido fib
sin tipo de firma se asumió que era monomórfica antes de que se terminó la generalización de todo el grupo de unión, la fib
subpartes recibieron exactamente el mismo tipo como a toda la fib
. Gracias a esto, se ve como fib
:
{-# LANGUAGE ScopedTypeVariables #-}
fib :: forall a. Num a => [a]
fib = fib'
where
fib' :: [a]
fib' = 0:1:zipWith (+) fib' (tail fib')
Y debido a que el tipo se mantiene fijo, puede utilizar sólo el diccionario dada en el arranque.
Un lado: razonar sobre el rendimiento en ghci es un poco extraño - es a) aproximadamente 30 veces más lento que ghc, yb) cualquier código de mundo real utilizará optimizaciones, por lo que las lecciones aprendidas en ghci no serán tan útiles. –