Tipi e classi
Haskell è un linguaggio di programmazione fortemente tipato, ovvero in cui solo le espressioni che rispettano alcune regole di tipo sono compilate e valutate. La specifica precisa delle regole di tipo di Haskell esula dagli scopi del corso. In questa scheda riassumiamo alcune nozioni fondamentali sui tipi di Haskell.
Tipi primitivi
In Haskell ci sono tipi predefiniti corrispondenti alle costanti numeriche e logiche usate fino ad ora. In particolare:
Int
è il tipo dei numeri interi a precisione finita (indicativamente corrisponde al tipoint
di Java);Integer
è il tipo dei numeri interi a precisione arbitraria (la dimensione massima dei numeri interi di questo tipo è limitata solo dalla memoria del calcolatore);Float
è il tipo dei numeri in virgola mobile a precisione singola (corrisponde al tipofloat
di Java);Double
è il tipo dei numeri in virgola mobile a precisione doppia (corrisponde al tipodouble
di Java);Bool
è il tipo dei valori booleani (indicativamente corrispondente al tipoboolean
di Java).
È possibile chiedere a GHCi di mostrare il tipo di una costante e,
in generale, di una espressione usando il comando :type
o la sua
abbreviazione :t
. Ad esempio, nel caso delle costanti booleane:
:type True
:type False
Se si richiede il tipo di una costante numerica, il risultato non è quello che ci si aspetta:
:type 1
:type 1.5
Il punto è che la costante 1
può avere tipi diversi a seconda
del contesto in cui è utilizzata. Potrebbe essere una costante
intera (a precisione finita) di tipo Int
, ma potrebbe anche essere
una costante intera (a precisione arbitraria) di tipo Integer
o
una costante in virgola mobile a precisione singola di tipo Float
o una costante in virgola mobile a precisione doppia di tipo
Double
. Da sola, la costante 1
non contiene abbastanza
informazioni per permettere ad Haskell di risolvere questa ambiguità
interpretativa, pertanto Haskell sostiene che il tipo di 1
ha la
forma
Num a => a
che è da leggersi come “un tipo sconosciuto a
che è istanza della
classe Num
”. Intuitivamente, una classe è un insieme di tipi
per i quali sono definite un insieme comune di funzioni. Senza
entrare nei particolari, per il momento ci accontentiamo di
apprezzare che Haskell “capisce” che 1
deve essere un numero
di qualche tipo, anche se non è in grado di dire precisamente di che
tipo.
È però possibile forzare il tipo di un’espressione usando una sintassi particolare:
:type 1 :: Int
:type 1 :: Integer
:type 1 :: Float
:type 1 :: Double
Un’espressione della forma E :: T
, dove E
è un’espressione e T
è un tipo, è semanticamente equivalente a (cioè ha lo stesso valore
di) E
, ma il programmatore indica esplicitamente il tipo T
che
si intende dare all’espressione. Il tipo T
specificato dal
programmatore deve essere una istanza del tipo che Haskell ha
inferito automaticamente per E
. Gli esempi qui sopra mostrano che
Int
, Float
e Double
sono tutte istanze della classe Num
.
Da notare che anche le costanti con la virgola, come 1.5
, non
comprendono sufficiente informazione per individuarne univocamente
il tipo:
:type 1.5
In questo caso, Haskell inferisce per 1.5
un tipo della forma
Fractional a => a
che indica come 1.5
abbia un tipo sconosciuto a
istanza della
classe Fractional
, la classe dei tipi che rappresentano “numeri
con la virgola”. In effetti, 1.5
potrebbe essere un numero in
virgola mobile a precisione singola o doppia, o anche un numero
razionale o complesso. Ad esempio, è possibile forzare il tipo di
1.5
a Float
o Double
:
:type 1.5 :: Float
:type 1.5 :: Double
Il tipo delle funzioni
Abbiamo avuto modo di constatare che una funzione è un’espressione
il cui tipo ha la forma T -> S
, in cui T
rappresenta il
dominio della funzione (è il tipo dei valori accettati come
argomento) mentre S
rappresenta il codominio della funzione (è
il tipo del valori prodotti come risultato). Ad esempio:
:type not
:type negate
Quando si applica una funzione a un argomento, il tipo dell’argomento deve coincidere (o essere istanza del) dominio della funzione e il tipo dell’intera applicazione è il codominio della funzione:
:type not True
:type negate 1
:type negate (1 :: Int)
:type negate (1 :: Integer)
Il tipo Num a => a -> a
di negate
è particolarmente interessante
perché molto ricco di informazione. Esso indica che negate
è una
funzione che accetta argomenti di qualunque tipo a
sia istanza di
Num
e produce risultati dello stesso tipo dell’argomento,
qualunque esso sia:
:type negate (1 :: Int)
:type negate (1 :: Float)
:type negate (1 :: Double)
Conversioni tra tipi numerici
L’operatore ::
non può essere usato per convertire numeri da un
tipo all’altro. Per esempio, un numero con virgola non può essere
convertito a numero intero semplicemente forzandone il tipo:
1.5 :: Int
Analogamente, un numero intero non può essere “promosso” a numero con virgola:
(1 :: Int) :: Float
Per effettuare queste conversioni occorre applicare una apposita funzione, scelta tra le seguenti:
fromIntegral :: (Integral a, Num b) => a -> b
truncate :: (RealFrac a, Integral b) => a -> b
round :: (RealFrac a, Integral b) => a -> b
In particolare, fromIntegral
promuove un qualsiasi numero intero
(il cui tipo è istanza della classe Integral
) a qualsiasi altro
tipo di numero. Le funzioni truncate
e round
rispettivamente
troncano e arrotondano un numero frazionario (il cui tipo è istanza
della classe RealFrac
) a un qualsiasi tipo di numero intero
(istanza della classe Integral
).
fromIntegral (1 :: Int) :: Float
truncate 1.5
round 1.5
Esercizi
- Quali delle seguenti espressioni sono ben tipate? Verificare le
risposte con GHCi:
1 `div` 2 1.5 `div` 2 1 / 2 1.5 / 2 (1 :: Int) / 2 (1 :: Float) / 2 (2 :: Int) + (3 :: Integer) (2 :: Float) <= 3 (2 :: Float) < (3 :: Int)
- Correggere le espressioni mal tipate dell’esercizio precedente
introducendo opportune conversioni di tipo e senza rimuovere
alcunché dell’espressione originale. In alcuni casi potrebbero
essere possibili soluzioni diverse usando conversioni diverse.
truncate 1.5 `div` 2 fromIntegral (1 :: Int) / 2 fromIntegral (2 :: Int) + (3 :: Integer) round (2 :: Float) < (3 :: Int)
- Concepire due espressioni il cui valore illustri chiaramente come
Int
sia un tipo a precisione finita (ci sono numeri troppo grandi per essere rappresentati con 32, 64 o 128 bit) eInteger
sia un tipo a precisione arbitraria (ci sono numeri di tipoInteger
che evidentemente non possono essere rappresentati in 32, 64 o 128 bit).(2 ^ 1024) :: Int (2 ^ 1024) :: Integer