Innanzitutto, il codice che hai fornito:
fact :: Num a => Int -> a
fact n = fromInteger (product [1..n])
non digita il controllo. Quindi, questa è una buona ragione per non scrivere la funzione in questo modo. Il problema è che n
è di tipo Int
, quindi product [1..n]
è anche di tipo Int
, ma fromInteger
prevede un Integer
, non un Int
.
Potresti sostituire fromInteger
con fromIntegral
, ma non dovresti. Osservare:
fact1 :: Num a => Int -> a
fact1 n = fromIntegral (product [1..n])
e in GHCi:
> fact1 10
3628800
> fact1 100
0
> fact1 100 :: Double
0.0
> fact1 100 :: Integer
0
>
In confronto, un'attuazione ragionevole di un% di polimorfico% co_de potrebbe essere simile a:
fact2 :: (Integral n, Num a, Enum a) => n -> a
-- OR: fact2 :: (Num a, Enum a) => Int -> a
fact2 n = product [1..fromIntegral n]
e
> fact2 100 :: Int
0 -- correct value in light of 'Int' overflow
> fact2 100 :: Integer
9332...long number here...000 -- correct for infinite-precision integer
> fact2 100 :: Double
9.33262154439441e157 -- right again, when working with 'Double's
>
Quindi, per rispondere alla tua domanda, sì , stai perdendo qualcosa di desiderabile che il sistema di tipi fa per te.
In particolare, il sistema dei tipi rende più difficile la conversione senza pensieri tra diversi tipi numerici che possono determinare gli attributi di un tipo numerico (ad esempio fact
) che inaspettatamente si perde negli usi apparenti di altri tipi numerici (ad esempio, Int
). Se lanci Integer
e altre conversioni polimorfiche in funzioni matematiche senza considerare attentamente l'implementazione, è probabile che introduca errori di questo tipo.
Quindi, come dovresti scrivere funzioni matematiche in Haskell?
Bene, come approccio generale, le funzioni matematiche di Haskell polimorfiche sono scritte e scritte per funzionare all'interno di uno specifico tipo numerico nella misura del possibile. Pertanto, la firma di fromIntegral
è:
(+) :: (Num a) => a -> a -> a
significa che gli argomenti hanno ciascuno lo stesso tipo e che sarà anche il tipo del risultato. Se sto utilizzando un (+)
, so che l'overflow potrebbe essere un problema. Se sto usando un Int
, so che non lo farà. Se sto usando un Integer
, so che aggiungerne uno a un numero enorme potrebbe essere uguale al numero enorme, ma almeno non sarà negativo (come con Double
).
Se combino un sacco di calcoli:
myRoot :: Float -> Float -> Float
myRoot a b c = (-b + sqrt (b*b - 4*a*c)) / (2*a)
So che l'intero calcolo avviene con operazioni su Int
s e non coinvolge segretamente Float
-to- Float
-to- Double
conversioni.
Dovresti provare a scrivere le tue stesse funzioni matematiche in questo modo e lasciare le conversioni all'utente della tua funzione, dove saranno chiare ed esplicite, e l'utente avrà (si spera) dimostrato di sapere cosa sta facendo .
Ora, Float
è in realtà un po 'un caso speciale. Se avessi seguito il consiglio sopra (ad esempio, scrivere funzioni che funzionano con un singolo tipo numerico), allora potresti preferire una firma più simile a:
fact3 :: (Num a, Enum a) => a -> a
fact3 n = product [1..n]
Questo è ciò che suggerito da @gigabytes. Probabilmente va bene, ma può portare a risultati strani:
> fact3 2.5
6.0
>
Il problema è che fact
ha senso solo come una funzione fattoriale quando viene applicata agli interi, e l'applicazione sconsiderata a un non intero probabilmente rappresenta un errore di programmazione. Meglio lasciare che il sistema di tipi dia una mano usando uno dei tipi di firma alternativi sopra:
> fact2 2.5
...type error...
>
Se il programmatore veramente voleva prendere un fattoriale di un non intero, lui o lei dovrebbe rendere esplicito il comportamento desiderato:
> fact2 (floor 2.5)
2
> fact2 (ceiling 2.5)
6
>
Il ragionamento simile conduce a tipi come:
round :: (RealFrac a, Integral b) => a -> b
e gli operatori di alimentazione multipla:
(**) :: Floating a => a -> a -> a
(^) :: (Integral b, Num a) => a -> b -> a
(^^) :: (Fractional a, Integral b) => a -> b -> a
che - eccetto fact
- non segue il modello "lavora in un tipo".
Tutto questo parla della tua ultima domanda. Il motivo per cui operazioni come:
(/) :: (Fractional a) => a -> a -> a
avere tipi restrittivi non è che il sistema tipo non possa esprimere "usa qualsiasi tipo sia più grande", è che i programmatori Haskell non VOGLIONO il sistema tipo per esprimere quel concetto, almeno nel contesto di diversi tipi numerici. Valutano il fatto che (**)
e (/)
sono operatori diversi e che il tipo di div
rende chiaro che l'operazione si svolgerà all'interno di un particolare tipo numerico senza implicite coercizioni dietro al retro del programmatore.
Lo svantaggio è che centinaia di nuovi programmatori Haskell sono stati ostacolati da:
mean xs = sum xs / length xs
ma in genere è stato considerato un piccolo prezzo da pagare.