Давая явную сигнатуру типа, предотвратить GHC от делать определенные предположения о вашем коде. Я покажу пример (взятый из этого question):
foo (x:y:_) = x == y
foo [_] = foo []
foo [] = False
Согласно GHCi, тип этой функции Eq a => [a] -> Bool
, как можно было ожидать. Однако, если вы объявите foo
с этой подписью, вы получите ошибку «неоднозначной переменной типа».
Причина, по которой эта функция работает только без сигнатуры типа, связана с тем, как работает проверка типов в GHC. Когда вы опускаете подпись типа, предполагается, что foo
имеет монотип [a] -> Bool
для определенного фиксированного типа a
. Как только вы закончите набирать группу привязки, вы обобщаете типы. Вот где вы получаете forall a. ...
.
С другой стороны, когда вы объявляете полиморфный тип подписи, вы явно указать, что foo
является полиморфным (и, таким образом, тип []
не должен совпадать с типом первого аргумента) и стрелы, вы получите неоднозначный тип переменная.
Теперь, зная об этом, давайте сравним ядро:
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
И второй:
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 }
С явной сигнатуры типа, как и с foo
выше, GHC должен относиться fib
, как потенциально полиморфно рекурсивное значение. Мы могли передать несколько разных Num
словарь на fib
в zipWith (+) fib ...
, и в этот момент нам пришлось бы отбросить большую часть списка, так как разные Num
означает разные (+)
. Конечно, после компиляции с оптимизацией GHC замечает, что словарь Num
никогда не изменяется во время «рекурсивных вызовов» и оптимизирует его.
В ядре выше вы можете увидеть, что GHC действительно дает fib
словарь Num
(названный $dNum
) снова и снова.
Поскольку fib
без типа подписи принимались мономорфным перед обобщением всей группы связывания было закончено, то fib
подразделов получили точно такой же тип, как и весь fib
. Благодаря этому, fib
выглядит следующим образом:
{-# LANGUAGE ScopedTypeVariables #-}
fib :: forall a. Num a => [a]
fib = fib'
where
fib' :: [a]
fib' = 0:1:zipWith (+) fib' (tail fib')
И потому, что тип остается фиксированной, вы можете использовать только один словарь заданный при запуске.
В стороне: рассуждение о производительности в ghci немного странно - его а) примерно в 30 раз медленнее, чем сам ghc, и б) любой код реального мира будет использовать оптимизацию, поэтому уроки, извлеченные в ghci, не будут полезны. –