Enumerazioni
Una enumerazione è un tipo di dato i cui valori sono in numero
finito e dunque enumerati al momento della definizione del tipo di
dato. Il tipo Bool
è un esempio di enumerazione i cui valori
sono False
e True
. In questa scheda vediamo come definire
una nuova enumerazione.
Definizione di una enumerazione
Supponiamo di voler realizzare una funzione morra
che determini il
vincitore di due giocatori che si sfidano alla morra
cinese. La funzione ha
due argomenti con le mosse dei due giocatori e restituisce un
numero tra 0, 1 o 2 a seconda che il gioco finisca in parità (le due
liste sono uguali) o che vinca uno dei due giocatori. Siccome le
mosse del gioco sono di tre tipi, possiamo rappresentarle definendo
la seguente enumerazione:
data Mossa = Sasso | Carta | Forbici
La definizione è introdotta dalla parola chiave data
seguita dal
nome scelto per il tipo, dal simbolo =
e dall’elenco dei
costruttori del tipo di dato. Il nome del tipo definito
(nell’esempio, Mossa
) deve iniziare con una lettera maiuscola e
può essere usato ovunque possa comparire un tipo. I costruttori
devono iniziare con una lettera maiuscola e possono essere usati per
costruire valori di tipo Mossa
. Nel caso specifico, Sasso
,
Carta
e Forbici
sono tutti e soli i valori di tipo Mossa
.
:type Sasso
:type Carta
:type Forbici
Il nuovo tipo di dato si integra automaticamente con quelli già
definiti. Per esempio, è possibile scrivere tuple e liste al cui
interno si trovano valori di tipo Mossa
.
:type [Sasso, Carta, Forbici]
length [Sasso, Carta, Forbici]
Il tipo Mossa
è “nuovo” nel senso che è incompatibile con tutti
gli altri tipi già esistenti. In particolare, non è possibile
confondere numeri e mosse:
[Sasso, 0]
Visualizzazione
Senza alcuna dichiarazione aggiuntiva, Haskell non sa cosa voglia dire “visualizzare” il valore di un nuovo tipo di dato. Se si tenta di farlo, Haskell segnala un errore:
Sasso
Come si evince dal messaggio di errore che si ottiene in questo
caso, Haskell si lamenta del fatto che il tipo Mossa
non è istanza
della classe Show
, ovvero la classe dei tipi di dato
“visualizzabili”.
È possibile chiedere ad Haskell di introdurre una visualizzazione di default per un nuovo tipo di dato aggiungendo una apposita clausola alla definizione del tipo stesso:
data Mossa = Sasso | Carta | Forbici
deriving Show
Usando la clausola deriving Show
, Haskell fa in modo che la
visualizzazione di un costruttore sia il nome del costruttore
stesso:
Sasso
[Sasso, Carta, Forbici]
Pattern matching
Una volta definito un nuovo tipo di dato, è possibile usare il
pattern matching per analizzarne i valori. Per esempio, la
seguente funzione vince
accetta le mosse di due giocatori e
stabilisce chi vince o se le due mosse sono uguali:
vince :: Mossa -> Mossa -> Int
vince Sasso Carta = 2 -- vince il giocatore 2
vince Sasso Forbici = 1 -- vince il giocatore 1
vince Carta Sasso = 1
vince Carta Forbici = 2
vince Forbici Sasso = 2
vince Forbici Carta = 1
vince _ _ = 0 -- in tutti gli altri casi, parità
A questo punto è possibile completare la definizione di morra
analizzando la struttura delle due liste di mosse dei due giocatori:
morra :: [Mossa] -> [Mossa] -> Int
morra [] [] = 0
morra _ [] = 1
morra [] _ = 2
morra (x : xs) (y : ys) | vincitore /= 0 = vincitore
| otherwise = morra xs ys
where
vincitore = vince x y
La prima equazione considera il caso in cui entrambe le liste sono
vuote, che significa che i giocatori hanno smesso di giocare allo
stesso momento, e si ha parità. Le due equazioni successive
considerano il caso in cui uno dei due giocatori fa una mossa e
l’altro no, determinando la vittoria del giocatore attivo. L’ultima
equazione confronta la prima mossa fatta dai due giocatori. Se
questa mossa determina la vittoria di uno dei giocatori, il gioco
termina. In caso contrario, la funzione analizza le mosse
successive. Si noti la dichiarazione locale di vincitore
, comoda
in questo caso per evitare di calcolare due volte il valore di
vince x y
.
morra [Sasso,Carta,Carta] [Sasso,Carta]
morra [Sasso,Carta,Carta] [Sasso,Carta,Forbici]
morra [Sasso,Carta,Sasso] [Sasso,Carta,Forbici]
Il tipo unitario
Un caso estremo di enumerazione che useremo in seguito è quello in
cui vi è un solo costruttore. Tale enumerazione prende il nome di
“tipo unitario”. Haskell usa la sintassi ()
per indicare sia il
tipo unitario che il suo unico costruttore. In altre parole, il tipo
unitario sarebbe definito così:
data () = ()
:type ()
Il valore unitario è privo di informazione. Infatti, sapendo che un
valore è di tipo ()
si sa anche che il valore è ()
. In
generale, la valutazione di un’espressione è di tipo ()
o non
termina o produce ()
.
Esercizi
- Definire un tipo
PuntoCardinale
con i costruttoriNord
,Sud
,Ovest
edEst
. Definire le funzionisinistra :: PuntoCardinale -> PuntoCardinale
,destra :: PuntoCardinale -> PuntoCardinale
eindietro :: PuntoCardinale -> PuntoCardinale
in modo tale che solo una di queste funzioni usi il pattern matching.data PuntoCardinale = Nord | Sud | Ovest | Est deriving Show sinistra :: PuntoCardinale -> PuntoCardinale sinistra Nord = Est sinistra Est = Sud sinistra Sud = Ovest sinistra Ovest = Nord destra :: PuntoCardinale -> PuntoCardinale destra = indietro . sinistra indietro :: PuntoCardinale -> PuntoCardinale indietro = sinistra . sinistra
- Definire un tipo
Giorno
i cui costruttori sono i giorni della settimanaLun
,Mar
,Mer
,Gio
,Ven
,Sab
,Dom
poi definire le seguenti funzioni:domani :: Giorno -> Giorno
.fra :: Int -> Giorno -> Giorno
che, applicata a un numero non negativo $n$ e a un giorno $g$, calcoli il giorno della settimana corrispondente a $n$ giorni dopo $g$.- Usando la funzione di libreria
replicate
, ridefinire la funzionefra :: Int -> Giorno -> Giorno
del punto precedente senza fare uso esplicito della ricorsione.
data Giorno = Lun | Mar | Mer | Gio | Ven | Sab | Dom deriving Show domani :: Giorno -> Giorno domani Lun = Mar domani Mar = Mer domani Mer = Gio domani Gio = Ven domani Ven = Sab domani Sab = Dom domani Dom = Lun fra :: Int -> Giorno -> Giorno fra 0 = id fra n = domani . fra (n - 1) fra_replicate :: Int -> Giorno -> Giorno fra_replicate n = foldr (.) id (replicate n domani)
- La classe
Ord
ha anche una funzionecompare
che restituisce un valore di tipoOrdering
. Esaminando il tipo dicompare
e la definizione diOrdering
(con:info Ordering
) descrivere comportamento e utilità dicompare
.La funzione
compare
restituisce il risultato del confronto di due valori $x$ ed $y$ il cui tipo è istanza diOrd
, dunque per i quali esiste una relazione d’ordine totale: se il risultato èLT
allora $x$ è “più piccolo” di $y$; se il risultato èGT
allora $x$ è “più grande” di $y$; se il risultato èEQ
allora $x$ ed $y$ sono uguali. È conveniente usarecompare
laddove il confronto di due valori può essere “costoso” (ad esempio, se $x$ ed $y$ sono liste) ed è preferibile confrontarli una volta sola (concompare
) invece che più volte (prima con<
, poi con>
e infine con==
) per capire in che relazione sono. - In un linguaggio di programmazione puro come Haskell, in cui le
funzioni non possono avere effetti collaterali, il tipo
()
è apparentemente inutile. Perché? Si pensi ad esempio a funzioni di tipo() -> T
oT -> ()
.Una funzione di tipo
() -> T
è necessariamente una funzione costante perché “sa” che sarà applicata a()
, l’unico valore di tipo()
. Dunque anziché scriverla come funzione costante è sufficiente definire la costante di tipoT
. Una funzione di tipoT -> ()
applicata a un argomento di tipoT
non termina o produce()
, l’unico valore di tipo()
. Dunque tanto vale scrivere direttamente()
invece di applicare la funzione.