DEFINIZIONE DI NUOVI TIPI Un tipo `e un insieme di valori. Per

Transcript

DEFINIZIONE DI NUOVI TIPI Un tipo `e un insieme di valori. Per
1
DEFINIZIONE DI NUOVI TIPI
Un tipo è un insieme di valori.
Per definire un nuovo tipo occorre specificare:
1. un nome per il tipo
2. come costruire i valori del tipo, cioè quali sono i costruttori del tipo.
I tipi enumerati
Sono tipi costituiti da un insieme finito di valori. Si possono definire semplicemente
elencando i valori del tipo.
Ad esempio, il tipo predefinito bool è un tipo enumerato.
I valori di un tipo enumerato sono identificati da costanti, il modo più semplice di
costruire un valore del tipo.
Una costante è dunque un caso particolare di costruttore.
Un nuovo tipo viene definito mediante una dichiarazione di tipo.
2
Esempio: il tipo delle quattro direzioni principali
{Su, Giu, Destra, Sinistra}
Dichiarazione del nuovo tipo direzione:
type direzione = Su | Giu | Destra | Sinistra;;
• type: parola chiave che introduce la dichiarazione di un nuovo tipo;
• direzione: nome del tipo;
• Su, Giu, Destra, Sinistra: valori del nuovo tipo.
• I nomi dei costruttori di tipi definiti dal programmatore iniziano sempre con una
lettera maiuscola.
• Nella dichiarazione, i valori del tipo sono separati dalla barra verticale.
3
Una dichiarazione di tipo introduce nuovi valori
Prima della dichiarazione, OCaml non conosce i valori Su, Giu, Destra, Sinistra.
# Destra;;
Characters 0-6:
Destra;;
^^^^^^
Unbound constructor Destra
Dopo la dichiarazione:
# type direzione = Su | Giu | Destra | Sinistra;;
type direzione = Su | Giu | Destra | Sinistra
# Destra;;
- : direzione = Destra
I nuovi valori possono essere utilizzati nella costruzione di altri oggetti:
# [Su; Destra];;
4
- : direzione list = [Su; Destra]
# [(Giu,1); (Sinistra,2); (Su,3)];;
- : (direzione * int) list = [(Giu, 1); (Sinistra, 2); (Su, 3)]
5
Esempio: la rappresentazione delle carte napoletane
Definiamo un tipo per rappresentare i semi:
type seme = Bastoni | Coppe | Denari | Spade;;
e un tipo per rappresentare i valori:
type valore = Asso | Due | Tre | Quattro | Cinque
| Sei | Sette | Fante | Cavallo | Re;;
Una carta del mazzo è allora rappresentabile mediante una coppia di tipo valore *
seme.
# let settebello =(Sette,Denari);;
val settebello : valore * seme = (Sette, Denari)
# let briscola = Spade
in [(Asso,Denari);(Due,briscola);(Tre,Bastoni)];;
- : (valore * seme) list = [(Asso, Denari); (Due, Spade); (Tre, Bastoni)]
Si noti che i tipi enumerati sono tipi con uguaglianza:
6
# List.mem (Due,Spade) [(Asso,Denari);(Due,Spade);(Tre,Bastoni)];;
- : bool = true
7
Le nuove costanti introdotte sono costruttori del tipo
Quindi possono occorrere in un pattern.
E possiamo definire operazioni usando il pattern matching:
(* valore : valore -> int *)
let valore = function
Asso -> 1
| Due -> 2
| Tre -> 3
| Quattro -> 4
| Cinque -> 5
| Sei -> 6
| Sette -> 7
| Fante -> 8
| Cavallo -> 9
| Re -> 10;;
# valore Fante;;
- : int = 8
8
# (valore Due) + (valore Asso);;
- : int = 3
9
Rappresentazione di posizioni e movimenti su un piano
Vogliamo rappresentare la posizione ed il movimento di un oggetto su un piano.
L’oggetto:
• si trova in un punto del piano, identificato da una coppia (x, y) di interi;
10
• è rivolto in una delle quattro direzioni principali (Su, Giu, Destra o Sinistra);
• può spostarsi in avanti di n punti: procede nella direzione verso cui è rivolto.
• può girare in senso orario, modificando la sua posizione di 90o.
11
Problema: data la posizione (punto del piano (x, y) e una direzione) dell’oggetto
e un’azione di spostamento o di cambiamento di direzione, calcolare la nuova
posizione dell’oggetto.
Ad esempio:
• se l’oggetto si trova in (4, 3) ed è rivolto verso Destra, quando va avanti di 3 passi
si troverà in (7, 3), sempre rivolto verso Destra;
• se l’oggetto si trova in (−2, −3) ed è rivolto verso Giu, quando gira si troverà
sempre in (−2, −3), ma rivolto verso Sinistra.
Posizione: punto e direzione
Punto: coppia di interi
Rappresentazione delle direzioni
type direzione = Su | Giu | Destra | Sinistra;;
12
Rappresentazione delle posizioni
Una posizione è identificata da una coppia di coordinate (punto nel piano) e una
direzione.
Possiamo rappresentare le posizioni mediante triple di tipo int * int * direzione,
oppure definire opportunamente un nuovo tipo di dati:
type posizione = Pos of int * int * direzione
• Pos è l’unico costruttore di valori di tipo posizione.
È un costruttore funzionale: si comporta come una funzione
Pos:
int * int * direzione -> posizione
• La dichiarazione del tipo posizione specifica che i valori del tipo posizione sono
tutte le espressioni della forma
Pos(x,y,d)
dove x e y sono valori di tipo int e d è un valore di tipo direzione.
La parola chiave of introduce il tipo degli oggetti a cui il costruttore si applica per
costruire un oggetto di tipo azione.
13
• I costruttori sono parte della descrizione del tipo, determinano il modo canonico
di descrivere i valori del tipo.
14
• Mediante il pattern matching è possibile definire i selettori del tipo:
(* xcoord : posizione -> int *)
let xcoord (Pos(x,_,_)) = x
(* ycoord : posizione -> int *)
let ycoord (Pos(_,y,_)) = y
(* dir : posizione -> direzione *)
let dir (Pos(_,_,d)) = d
• ATTENZIONE: il tipo posizione non è lo stesso tipo di int * int * direzione
# Pos (3,5,Su);;
- : posizione = Pos (3, 5, Su)
# (3,5,Su);;
- : int * int * direzione = (3, 5, Su)
# (3,5,Su) = Pos(3,5,Su);;
Characters 11-22:
15
(3,5,Su) = Pos(3,5,Su);;
^^^^^^^^^^^
This expression has type posizione but is here used with type
int * int * direzione
16
Esercizio
Qual è l’errore nella definizione seguente?
# let punto_di Pos(x,y,_) = (x,y);;
Characters 13-16:
let punto_di Pos(x,y,_) = (x,y);;
^^^
The constructor Pos expects 3 argument(s),
but is here applied to 0 argument(s)
17
Rappresentazione delle azioni
Ci sono due tipi di azioni:
• girare di 90o in senso orario
• andare avanti di n passi (con n intero)
Vogliamo definire un tipo di dati azione per rappresentare le azioni.
L’azione di girare può essere rappresentata da una costante: Gira.
L’azione di andare avanti di n passi costituisce in realtà tutto un insieme di azioni:
andare avanti di 0 passi, andare avanti di 1 passo, andare avanti di 2 passi, ... E anche
andare avanti di −1 passo (indietro di 1 passo), avanti di −2 passi (indietro di 2), ...
Per rappresentare le azioni serve una funzione: un costruttore funzionale che,
applicato a un intero n, riporta il valore che rappresenta “andare avanti di n passi”:
Avanti 0, Avanti 1, Avanti 2, Avanti 3, ...
Avanti(-1), Avanti(-2), Avanti(-3), ....
18
Dichiarazione di un tipo per rappresentare azioni
type azione =
Gira
| Avanti of int;;
Questa dichiarazione specifica che l’insieme dei valori di tipo azione è costituito da:
• la costante Gira;
• tutti i valori della forma Avanti n, dove n è un intero.
Avanti è un costruttore funzionale
• è una funzione di tipo int -> azione;
• quando è applicato a un valore di tipo int, restituisce (costruisce) un valore di
tipo azione.
I valori del tipo azione sono dunque:
Gira,
Avanti 0, Avanti 1, Avanti 2, Avanti 3, ...
Avanti (-1), Avanti (-2), Avanti (-3), ....
19
L’insieme dei valori di tipo azione è infinito.
20
La soluzione del problema
Operazione principale:
sposta : posizione * azione -> posizione
sposta(pos,act):
se act ha la forma:
Gira: allora riporta la posizione che si ottiene girando
la direzione di pos di 90 gradi in senso orario
Avanti n: allora riporta la posizione che si ottiene spostando
avanti di n passi le coordinate di pos
Sottoproblema 1: data una direzione, riportare la direzione che si ottiene girando
di 90o in senso orario.
Sottoproblema 2: data una posizione Pos(x,y,d) e un intero n, riportare la posizione Pos(x’,y’,d) che si ottiene andando avanti di n passi nella direzione
indicata da d.
21
Soluzione del sottoproblema 1
Si vuole definire una funzione
gira:
direzione -> direzione
che applicata a Su riporti Destra, applicata a Giu riporti Sinistra, applicata a
Destra riporti Giu e applicata a Sinistra riporti Su:
(* gira : direzione -> direzione *)
let gira d =
if d=Su then Destra
else if d=Giu then Sinistra
else if d=Destra then Giu
else Su (* d=Sinistra *)
Osservazione: il nome della funzione gira inizia con lettera minuscola: è diverso dalla
costante Gira.
Ma possiamo anche definire la funzione usando il pattern matching: i costruttori
possono occorrere in un pattern
(* gira : direzione -> direzione *)
let gira = function
22
Su -> Destra
| Giu -> Sinistra
| Destra -> Giu
| Sinistra -> Su
23
Soluzione del sottoproblema 2
Vogliamo definire una funzione
avanti :
posizione * int -> posizione
che, applicata a una posizione Pos(x,y,dir) e un intero n, a seconda della direzione
dir, incrementi o decrementi x o y:
• se dir è Su: da (x,y) si passa a (x,y+n)
• se dir è Giu: da (x,y) si passa a (x,y-n)
• se dir è Destra: da (x,y) si passa a (x+n,y)
• se dir è Sinistra: da (x,y) si passa a (x-n,y)
(* avanti : posizione * int -> posizione *)
let avanti (Pos(x,y,dir),n) =
match dir with
Su -> Pos(x,y+n,dir)
| Giu -> Pos(x,y-n,dir)
| Destra -> Pos(x+n,y,dir)
| Sinistra -> Pos(x-n,y,dir)
24
Soluzione del problema principale
(* sposta : posizione * azione -> posizione *)
let sposta (Pos(x,y,dir),act) =
match act with
Gira -> Pos(x,y,gira dir)
(* le coordinate non cambiano,
la direzione gira di 90 gradi in senso orario *)
| Avanti n -> avanti(Pos(x,y,dir),n)
# sposta(Pos(4,3,Destra),Avanti 3);;
- : posizione = Pos (7, 3, Destra)
# sposta(Pos(-2,-3,Giu), Gira);;
- : posizione = Pos (-2, -3, Sinistra)
25
Esercizio
Utilizzando la funzione sposta, definire una funzione
esegui:
posizione * azione list -> posizione
che, applicata a una posizione e una lista di azioni [a1,a2,...,an] riporti la posizione
in cui si trova l’oggetto che, trovandosi inizialmente nella posizione data, esegue in
sequenza (e in quest’ordine) le azioni a1,a2,...,an.
Ad esempio si deve avere:
# let p=Pos(0,0,Su);;
val p : posizione = Pos (0, 0, Su)
# esegui(p, [Avanti 3; Gira]);;
- : posizione = Pos (0, 3, Destra)
# esegui(p, [Avanti 3; Gira; Avanti 2]);;
- : posizione = Pos (2, 3, Destra)
# esegui(p, [Avanti 2; Gira; Avanti 3]);;
- : posizione = Pos (3, 2, Destra)
26
# esegui(p, [Avanti 2; Gira; Gira; Gira; Avanti 3]);;
- : posizione = Pos (-3, 2, Sinistra)
27
Unione di tipi
type number = Int of int
| Float of float;;
(* sum : number * number -> number *)
let sum = function
(Int x,Int y) -> Int (x + y)
| (Int x,Float y) -> Float ((float x) +. y)
| (Float x,Float y) -> Float (x +. y)
| (Float x,Int y) -> Float(x +. float y);;
In nuovo tipo rappresenta l’unione disgiunta dei tipi che sono gli argomenti dei
costruttori
·
A ∪ B = {(a, 0) | a ∈ A} ∪ {(b, 1) | b ∈ B}
I costruttori marcano gli elementi, rendendo riconoscibile la loro provenienza.
28
Tipi definiti ricorsivamente
Definizione ricorsiva dei numeri naturali:
(i) 0 ∈ IN
(ii) per ogni n ∈ IN, succ(n) ∈ IN
(iii) gli unici elementi di IN sono quelli che si ottengono mediante (i) e (ii).
Possiamo definire ricorsivamente un nuovo tipo per la rappresentazione dei numeri
naturali:
type nat = Zero | Succ of nat;;
I valori di tipo nat sono:
Zero, Succ Zero, Succ(Succ Zero), Succ(Succ(Succ Zero)), ...
Sui tipi di dati definiti induttivamente si possono definire operazioni ricorsivamente:
(* int_of_nat : nat -> int *)
(* int_of_nat n = valore intero corrispondente a n *)
let rec int_of_nat = function
Zero -> 0
| Succ n -> succ(int_of_nat n)
29
# int_of_nat (Succ (Succ (Succ (Succ Zero))));;
- : int = 4
30
L’operazione somma di numeri naturali
Si può definire ricorsivamente sul primo argomento:
(* somma : nat * nat -> nat *)
let rec somma (n,m) =
match n with
Zero -> m
| Succ k -> Succ(somma(k,m));;
# let k = somma(Succ(Succ Zero), Succ(Succ(Succ Zero)));;
val k : nat = Succ (Succ (Succ (Succ (Succ Zero))))
# int_of_nat k;;
- : int = 5
31
DEFINIZIONE DI COSTRUTTORI DI TIPI
La definizione di un tipo può essere parametrica:
• con variabili di tipo
• con costruttori polimorfi
type ’a list = Nil | Cons of ’a * ’a list
Possiamo definire un costruttore di tipi (un tipo polimorfo) per le matrici di interi,
matrici di stringhe, matrici di liste di booleani, ...
type ’a matrix = Mat of ’a list list
matrix è un costruttore di tipi
Il costruttore
Mat:
è polimorfo
’a list list -> ’a matrix
32
Mat [[true;false];[false;true]] :
Mat [[1;2;3];[4;5]] : int matrix
bool matrix
33
Il tipo ’a option: un’alternativa all’uso delle eccezioni
type ’a option = None | Some of ’a
Una funzione parziale di tipo ’a -> ’b può essere implementata da una funzione di
tipo ’a -> ’b option.
Utile quando non si vuole propagazione degli errori.
(* new_assoc : (’a * ’b) list -> ’a -> ’b option *)
let rec new_assoc lst x = match lst with
[] -> None
| (k,v)::rest -> if x=k then Some v
else new_assoc rest x
# let alist = [(0,"pippo");(1,"pluto");(2,"paperino")];;
val alist : (int * string) list = [(0, "pippo"); (1, "pluto"); (2, "paperino")]
# new_assoc alist 2;;
- : string option = Some "paperino"
#
new_assoc alist;;
# new_assoc alist 3;;
- : string option = None
34
- : int -> string option = <fun>
# List.map (new_assoc alist) [1;5;3;0;4];;
- : string option list = [Some "pluto"; None; None; Some "pippo"; None]
35
Espressioni aritmetiche
Consideriamo espressioni costruite a partire da variabili e costanti intere mediante
applicazione delle operazioni di somma, sottrazione, prodotto e divisione (intera).
Ad esempio:
(3 + (5 × x))/(9 − y)
L’insieme delle espressioni aritmetiche si può definire induttivamente:
(i) se n è un numero intero, allora n è un’espressione;
(ii) se x è una variabile, allora x è un’espressione;
(iii) se E1 e E2 sono espressioni, allora anche (E1 +E2), (E1 −E2), (E1 ×E2) e (E1/E2)
sono espressioni;
(iv) nient’altro è un’espressione.
Osservazioni
1. L’insieme cosı̀ definito contiene un numero infinito di “oggetti di base”.
36
2. Ci sono quattro “costruttori funzionali” (somma, sottrazione, moltiplicazione, divisione): quattro modi diversi di costruire oggetti complessi a partire da oggetti
più semplici.
3. I costruttori funzionali si applicano a due oggetti più semplici per costruire un
oggetto più complesso.
37
Strutture ad albero
Poiché i costruttori funzionali si applicano a più di un oggetto la struttura di un’espressione aritmetica non è lineare (come quella di una lista), ma è una struttura ad
albero.
38
×
5×x
5
x
+
(3 + (5 × x))
×
3
5
x
/
(3 + (5 × x))/(9 − y)
−
+
9
×
3
5
x
y
39
Rappresentazione delle espressioni in OCaml
Possiamo definire un tipo di dati induttivo, ricalcando la definizione induttiva delle
espressioni:
type expr =
Int of int
| Var of string
| Sum of expr * expr
| Diff of expr * expr
| Mult of expr * expr
| Div of expr * expr
espressione
4
4×x
7−3
3 + (y × 2)
rappresentazione
Int 4
Mult (Int 4, Var x)
Diff(Int 7,Int 3)
Sum (Int 3, Mult (Var y, Int 2))
Rappresentazione di ambienti
mediante liste associative:
[(x,5);(z,0);(y,4)] rappresenta l’ambiente
x
z
y
5
0
4
40
Calcolo del valore di un’espressione in un ambiente
Problema: data una ambiente env, rappresentato da una lista associativa, e un’espressione aritmetica E, calcolare il valore di E in env. Se qualche variabile che
occorre in E non è legata in env, sollevare un’eccezione.
Procedimento ricorsivo
Sui tipi di dati definiti induttivamente si possono definire operazioni ricorsivamente:
Casi base: Se expr è una costante, cioè ha la forma Int n, allora il valore di expr
è n.
Se expr è una variabile, cioè ha la forma Var x, allora cercare il valore associato
a x in env.
Casi ricorsivi:
1. Se expr è una somma, cioè ha la forma Sum(e1,e2), allora il suo valore è la
somma dei valori di e1 e e2 in env.
2. Se expr è una differenza, cioè ha la forma Diff(e1,e2), allora il suo valore è
la differenza dei valori di e1 e e2 in env.
3. Se expr è un prodotto, cioè ha la forma Mult(e1,e2), allora il suo valore è il
prodotto dei valori di e1 e e2 in env.
41
4. Se expr è una divisione, cioè ha la forma Div(e1,e2), allora il suo valore è la
divisione dei valori di e1 e e2 in env.
Se e1 o e2 non hanno valore in env, allora expr non ha valore in env.
Ciascun caso ricorsivo corrisponde ad un modo di costruire un’espressione complessa
a partire da espressioni più semplici.
42
Il programma
(* eval : (string * int) list -> expr -> int *)
let rec eval env = function
Int n -> n
| Var x -> List.assoc x env
| Sum(e1,e2) -> (eval env e1) + (eval env e2)
| Diff(e1,e2) -> (eval env e1) - (eval env e2)
| Mult(e1,e2) -> (eval env e1) * (eval env e2)
| Div(e1,e2) -> (eval env e1) / (eval env e2)
# eval [] (Sum (Int 3, Mult (Int 4, Int 2)));;
- : int = 11
# let env = [("x",3);("y",0)];;
val env : (string * int) list = [("x", 3); ("y,0)];;\n", 0)]
# eval env (Sum (Var "x", Mult (Int 4, Int 2)));;
43
- : int = 11
# eval env (Sum (Var "x", Mult (Int 4, Var "z")));;
Exception: Not_found.
44
Dichiarazioni type che non introducono nuovi tipi
Supponiamo di voler definire un tipo specifico per la rappresentazione di ambienti come
liste associative. Possiamo farlo in due modi:
1. type (’a,’b) env = Env of (’a * ’b) list
(Il costruttore di tipo env si applica a una coppia di tipi)
In questo caso viene definito un nuovo tipo, diverso dal tipo delle (’a * ’b) list
2. type (’a,’b) env = (’a * ’b) list
Non è stato definito un nuovo tipo, ma solo un sinonimo per un
tipo già esistente.
# let
val e
# let
val e
e
:
e
:
= [(1,"a");(2,"b")];;
(int * string) list = [(1, "a"); (2, "b")]
= ([(1,"a");(2,"b")] : (int,string) env);;
(int, string) env = [(1, "a"); (2, "b")]
La definizione può servire come abbreviazione nei commenti, e a restringere il tipo
di funzioni o componenti di moduli
45
(* existing_link :
’a -> ’a list -> bool *)
let existing_link link env =
List.mem link env
(* val existing_link :
’a * ’b -> (’a, ’b) env -> bool *)
let existing_link link (env: (’a,’b)env) =
List.mem link env
46
Espressioni come alberi n-ari
Sfruttando l’associatività di somma e moltiplicazione, possiamo utilizzare costruttori
Sum e Mult che si applicano a liste di expr:
type expr =
Int of int
| Var of string
| Diff of expr * expr
| Div of expr * expr
| Sum of expr list
| Mult of expr list
Con questa struttura dati, un’espressione è sempre rappresentata da un’unico valore
di tipo expr?
47
Definizione ricorsiva di eval
È necessario modificare i casi ricorsivi 1 e 3
1. Se expr è una somma, cioè ha la forma Sum [e1;e2;...;ek], applicare ricorsivamente eval alle sotto-espressioni e1;e2;...;ek e calcolare la somma dei valori
ottenuti
1. Se expr è un prodotto, cioè ha la forma Mult [e1;e2;...;ek], applicare ricorsivamente eval alle sotto-espressioni e1;e2;...;ek e calcolare il prodotto dei
valori ottenuti.
eval env (Sum [e1;e2;...;ek])
:
[ e1;
e2;
...
ek ]
⇓
⇓
...
⇓
sumof [ eval env e1; eval env e2; ... eval env ek ]
48
Il programma
(* eval : (string * int) list -> expr -> int *)
let rec eval env = function
Int n -> n
| Var x -> List.assoc x env
| Diff(e1,e2) -> (eval env e1) - (eval env e2)
| Div(e1,e2) -> (eval env e1) / (eval env e2)
| Sum list -> let rec sumof = function
[] -> 0
| x::rest -> x+sumof rest
in sumof (List.map (eval env) list)
| Mult list -> let rec prodof = function
[] -> 1
| x::rest -> x*prodof rest
in prodof (List.map (eval env) list)
let env = [("x",3);("y",0)];;
let expr = Sum [Var "x";Int 4; Mult [Int 2; Var "x"; Diff (Var "y",Int 1)]];;
49
# eval’ env expr;;
- : int = 12
50
Esercizi
Scrivere programmi che risolvano i seguenti problemi:
1. Date due espressioni aritmetiche E1 e E2 determinare se E2 è una sottoespressione
di E1.
(Il concetto di sottoespressione è analogo a quello di sottoformula. Ad esempio, 3×8
è una sottoespressione di 5 + (3 × 8). Come caso particolare, qualsiasi espressione
è una sottoespressione di se stessa.)
2. Data un’espressione E, una variabile x e un’espressione E 0, costruire l’espressione
che si ottiene da E sostituendo ogni occorrenza di x con E 0.
3. Definire un tipo per rappresentare formule della logica proposizionale, e un tipo per
definire interpretazioni di un insieme di lettere proposizionali. Definire una funzione
che, applicata a un’interpretazione e una formula, riporti il valore di verità della
formula nell’interpretazione.