Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
PROGRAMMAZIONE
FUNZIONALE
Programmazione funzionale
PREMESSA
Questo pdf contiene informazioni utili per lesame di programmazione funzionale. Noi ce labbiamo messa tutta per cercare di farci capire, abbiamo usato un linguaggio semplice e informale. Abbiamo filtrato gli esempi, riportando solo quelli pi significativi. facile che troviate numerosi errori di battitura, scusateci, nessuno perfetto. Vi consigliamo ocmunque di non basarvi solo su questa guida per studiare, guardatevi le slide, fate esercizi prima sul pc, poi su carta, una volta fatto tutto questo, rivedetevi il pdf. Gli esempi verrano sempre riportati con il formato:
esempio
Mentre le operazioni svolte allinterno dellelaboratore, riportate per capire meglio il funzionamento del codice, che sono frutto della nostra interpretazione (in pratica il cammello al suo interno non fa esattamente quanto scritto, noi abbiamo rielaboratu tutto ci in manera da renderlo pi capibile) verranno riportate nel formato: step to step
Crediamo anche che questo pdf possa chiarire alcuni punti delle lezioni/slide che non avete capito. Notate bene che i contenuti fanno riferimento al materiale didattico trattato dal dottor Cimatti durante lanno accademico 2009/2010.
Albert Einstein (1879-1955) Quando le leggi della matematica si riferiscono alla realt, non sono certe; e quando sono certe, non si riferiscono alla realt.
Programmazione funzionale
INTRODUZIONE
Si dice Funzione un insieme delle coppie ordinate (ai; bi) appartenenti ad AxB dove A il dominio e B il codomino. Per ogni ai esiste sempre lo stesso bi. Si dice invece Funzione completa una funzione in cui per ogni ai esiste uno ed uno solo bi tale che (ai; bi) appartngono alla funzione. Dal punto di vista dellinformatica definiamo una funzione come un metodo di elaborazione di un risultato data una serie di argomenti. Un linguaggio funzionale un linguaggio non disrtuttivo, (non ha dati in in/out, ma solo dati di input). Esistono quindi solo funzioni, e non procedure, ricordiamo cos una funzione: Funzione A: a x : b y : c
Una funzione deve sempre restiturire (il classico return) un valore tipizzato, pu avere uno o pi dati di input (anchessi tipizzati), non pu esserne sprovvista. Ricordo che per i principi base dellinformatica i dati di input non possono essere modificati, possiamo immaginarli come di sola lettura. Lambiente (environment) si occupa ti tenere conto (o per meglio dire mappare) tutti i nomi di variabili, funzioni che sono legati ad una valore numerico, o comunque a qualcosaltro. Quando noi assegnamo il valore 5 ad esempio alla variabile x, diciamo che essa legata (bounded) a 5. Quando andiamo ad usare una variabile, il computer va nellambiente a vedere con quale valore collegata. Ecco un piccolo esempio di come si comporta lambiente, utilizzando OCAML. x+1 Unbounded value Non lega nulla ad x perch non una dichiarazione, bens un poerazione di somma fra interi Lega lintero 1 alla variabile x tipizzandola implicitamente ad intero (let in OCAML come il sia in matematica) Non lega nulla, ma restituisce 2, lambiente rimane invariato
let x = 1
X -> 1
x+1
X -> 1
Abbiamo parlato di variabili, ma non le abbiamo definite. Le variabili sono aree di memoria che possono assumere un valore numerico, interpretato come uno dei tipi fondamentali. Il loro nome deve iniziare con una lettera minuscola e pu contenere solo lettere, numeri, underscores e singoli apici. Esistono vari tipi standard, fra cui: Interi: (int) rappresentati da -231 a 231-1 su 32 bits Reali: (float) rappresentati in doppia precisione Bolleani: (bool) passono assumere il valore true oppure false. Caratteri: (char) memorizzano un carattere (ASCII con laggiunta di pochi altri caratteri) String: (string) memorizzano una serie di caratteri (non come un char[]).
Programmazione funzionale
possibile convertire i tipi fondamentali in altri tipi fondamentali, usando lespressione targettype_of_sourcetype. Ad esempio per convertire 1 in float si scriver float_of_int 1. Come in tutti i linguaggi, esistono degli operatori di base, questa volta, rispetto ad altri linguaggi, essi sono un po pi ostici, perch richiedono forti tipizzamenti, e se lavorano con variabili non tipizzate, le tipizzano implicitamente per poter restituire un risultato. Richiede due parametri interi, uno a sinistra ed uno a destra, restituisce un intero. int, int -> int: (param1, param2 -> result) Richiede due parametri float,restituisce un float. Float -> float -> float: (param1, param2 -> result). NB: 2 un intero, 2.0 un float Int -> int -> int. Moltiplica Float -> float -> float. Moltiplica Int, ->int -> int. Divide Float -> float -> float. Divide Float -> float -> Float. Elevamento a -> a -> Bool. Confronta (In seguito definito a) Int -> int -> int restituisce il resto della divisione fra i due parametri String -> string -> String. Concatena Bool -> bool -> bool. And Bool -> bool -> bool. Or Bool -> bool. Not
Int
Int
int
Un esempio per capire con pi facilit perch possiamo usare le funzioni come parametric il seguente. Lalbero di sintassi astratta di 3+(7*4) il seguente:
Int
int
Int
int
Notate che la funzione + si aspetta due parametri interi, quindi dobbiamo andare a vedere che tipo restituisce la *, essa restituisce un intero, quindi fin a questo punto lalbero ben tipizzato, ora dobbiamo esaminare il sottoalbero di *. Anchessa richiede due interi, quindi diciamo che tutto lalbero ben tipizzato. Un esempio di albero non ben tipizzato il seguente: a || (b+c). Lalbero si construisce come nel precedente, dallalto al basso. Step 1: bool ||
bool
bool?
Continuiamo verso il basso, anche se gia ci accorgiamo che + non ritorna un booleano: Step 2: Bool ||
Bool
int
int
In questo caso, la funzione || si aspetta due booleani, per la funzione + restituisce un intero, quindi il tutto non funzioner, lalbero si dice quindi non ben tipizzato.
FUNZIONI E DICHIARAZIONI
Una funzione prende uno o pi argomenti (parametri) e ne computa (restituisce) un risultato. In OCAML una funzione si dichiara con la seguente sintassi:
Programmazione funzionale
Si traduce come, sia add1 una funzione ad un parametro (che verr tipizzato nel corpo della funzione) x che svolge il lavoro di x+1. necessario ed obbligatorio chiudere la computazione della funzione con i doppi punto e virgola. Lalbero di sintassi astratta della nostra funzione : Int +
Int
int
In questo caso, abbiamo una variabile x che non stata precedentemente tipizzata, al contrario dei soliti linguaggi di programmazione che richiedono obbligatoriamente le dichiarazioni, OCAML, si arrangia, e capisce da solo che essa di tipo intero, in base alle operazioni che svlolger. Per capire di che tipo sono i parametri di una funzione, basta costruirne lalbero di sintassi astratta. Per richiamare la funzione appena dichiarata, la sintassi
add1 12;;
Qui si richiama la funzione che si chiama add1 e gli si passa come parametro il valore 12. Tale funzione restituir il valore 13. OCAML al suo interno, la rappresenta come:
int
add1
int
int
int
Evidentemente, lalbero ben tipizzato, e x assumer come valore il parametro passato (in questo caso 1). possibile inoltre utilizzare funzioni in fase di dichiarazione di altre:
Programmazione funzionale
In pratica, possible riutilizzare add1 dandogli come parametro lo stesso di add2 e modificarne il risultato, ritornandolo come computazione di add2. Per quanto rigurda la dichiarazione di variabili, tutto quell che c da sapere che ad esempio
let x = 2;;
come scrivere in c++ int x = 2. Nellambiente, la variabile x legata (bounded) al valore 2 (,x->2}). un po ostico capire come funzione lambiente, ma probabilmente con un esempio spegato, sar pi chiaro: let x = 2;; let add1 x = x+1;; {} {x->2}
Lambiente non contiene ancora informazioni, una volta dato linvio (riga successiva) conterr qualcosa Lambiente, finch non si preme invio, contiene le informazioni della computazione precedente, in questo caso che x legata al valore 2 (e quindi implicitamente tipizzata ad intero Lambiente ora conosce anche che il simbolo add1 corrisponde ad una funzione, e tiene conto anche delle computazioni precedenti, di cancellarle, a noi non interessa, si arrangia OCAML. Una volta richiesta listruzione add1 7, viene ricercato nellambiente il simbolo add1, si vede che una funzione che richiede un parametro, sicuramente la stringa che segue add1 il suo parametro. Il parametro si chiama x, ma un x diversa da quella dichiarata precedentemente, infatti se n aggiunta una in testa allambiente Una volta fatti tutti i passaggi interni non serve pi tenere conto del parametro (lultima x aggiunta nellambiente) e quindi, viene rimosso il binding. Alla fine della computazione, x legata a 2, come in partenza.
Risultato: 8
Le variabili, possono essere usate come variabili locali allinterno di funzioni: let x = 32;; let add1 y = x+1;; {} {x->32}
Come lesempio precedente Lambiente contiene le informazioni della computazione precedente finch non si preme invio. Il parametro y, non essendo utilizzato nel corpo della funzione, non viene tipizzato e quindi diciamo che di tipo a (qualsiasi tipo ammesso). Lo scheletro della funzione sar: val add1: a -> int = <fun> Come lesempio precedente Si lega ad y il sette e lo si tipizza ad intero, ma tanto non viene usato Come lesempio precedente.
{add1-> <fun>, x ->32} {y->7, add1-> <fun>, x ->2} {add1-> <fun>, x ->2}
Anche avessimo cercato di fare add1 A, dove A un carattere, y sarebbe stato tipizzato a carattere, legato ad A ed il tisultato sarebbe sempre stato 33, non ci sarebbero stati errori. una cosa inutile, ma concettualmente corretta. NB: a significa di qualsiasi tipo, come b, c ecc.
8 Altro esempio per capire meglio le funzioni ed i loro alberi di sintassi astratta:
Programmazione funzionale
float
2.0
+.
float
float
float
facile capire che media una funzione che richiede due parametric di tipo float, x ed y. Come sempre, i parametri devono essere ben tipizzati, altrimenti OCAML si arrabbia. Linterprete rappresenta la questa funzione come:
FUNZIONI PREDEFINITE
In OCAML esistono varie funzioni fornite dal linguaggio, che possono gestire lI/O, le operazioni sulle stringhe, funzioni aritmetiche (ad esempio la trigonometria), ecc. Ad esempio per stampare a video abbiamo:
val print_string: string -> unit = <fun> val print_char: char -> unit = <fun> val print_int: int -> unit = <fun> val print_float: float -> unit = <fun>
Il risultato di queste funzioni un tipo special, chiamato unit, paragonabile al tipo void del c++. C un solo valore di unit che (). Ad esempio:
Programmazione funzionale
IF THEN ELSE
la struttura di controllo condizionale pi semplice, funziona cone in tutti i normalissimi linguaggi e come nella vita reale: se (if) piove, allora (then) apri lomberello, altrimenti (else) impiccati. Ha sintassi:
int
int
Per vedere cosa ritorna lif bisogna vedere il tipo dellespressione che segue il then e lelse. In questo caso un float. <Alberi di sintassi astratta degli if ne abbiamo visti pochi, pertanto questo stato costruito a puro scopo dimostrativo, non seguitelo alla lettera perch potrebbe essere diverso da quello che vuole il prof> Gli if sono di fatto delle funzioni, richiedono parametri e ritornano valori sempre dello stesso tipo, funzionano solo in maniera un po diversa. Essendo simili a funzioni, possono essere passati come parametri.
string
true
bool
False
string
10
Programmazione funzionale
FUNZIONI ARITMETICHE
Per I numeri reali sono definite diverse funzioni predefinite, fra cui:
** : float *. float -> float sqrt : float -> float -. : float -> float *. : float *. float -> float /. : float * float -> float +. : float * float -> float 15.5 ** 2.0 sqrt 8.0 -1e10, -average 3.1416 *. r *. r 7.0 /. 3.5, score +. 1.0 Esponenziale Radice quadrata Negazione unaria Moltiplicazione Divisione Addizione
score -. 1.0
Sottrazione
Arrotonda allintero pi vicino per eccesso Arrotonda allintero pi vicino per difetto
exp, log, log10, cos, sin, tan, exp 10.0 acos, ... : float -> float
Le solite funzioni
+ : int * int -> int - : int * int -> int abs : int -> int
Programmazione funzionale
11
NB: su OCAML si pu facilmente individuare lo scheletro della funzione richiamandola senza passarne parametri. Ci sono vari modi per scrivere le funzioni, ovvio che la funzione + richiede due parametri, uno predicente ed uno successivo, possiamo comunque scrivere in maniera inutile
(+) 3 5;;
Avendo come risultato 8. La precedente una scrittura equivalente a 3+5.
PRODOTTO CARTESIANO
Dati gli insiemi S e T, si dice prodotto cartesiano di S e T, scritto come SxT, linsieme ,(s,t) | s contenuto in S e t contenuto in T}. Il prodotto cartesiano di S1 Sn definito come: {(s1sn) | s1 contenuto in S1 sn contenuto in Sn}. Alcuni valori possono essere combinati per formare un singolo valore. Ad esempio:
(1,2);;
Lo scheletro di questa combinazione
: int*int = (1,2)
Gli elementi di queste combinazioni (prodotti cartesiano) non devono essere necessariamente dello stesso tipo. Gli elementi del prodotto cartesiano fra insiemi vengono chiamati tuple. Possono essere comodi per computare molti valori contemporaneamente. In seguito un esempio di funzioni che richiedono una tupla come parametro e restituiscono una tupla come risultato.
12
Programmazione funzionale
RICORSIVIT
Premessa: la differenza che sta fra = ed == che il primo verifica lugualit della struttura, mentre l== confronta la loro posizione nellambiente. consigliato usare l= perch meno oscuro e cattivo dell==. Il costruttore let permette di definire variabili e funzioni, creando il binding fra il nome ed il valore. Questo costruttore non ritorna valori, ma il compilatore sa che lavoro fa, se definisce una variabile si accorge che sta definendo una variabile, se una funzione si accorge che una funzione. Questo costrutto non pu essere usato nelle espressioni. Molte funzioni in matematica sono definite su se stesse, questo si pu fare anche col CAMMELLO: il principio base della ricorsivit. Ad esempio, volessimo automatizzare una funzione di questo tipo:
1 n! = (n-1)!n
if n = 0 otherwise
Questa la classica funzione del fattoriale: continua a richiamare se stessa, diminuendo di 1 il parametro, finch esso diventa 1. Il codice nel cammello realizzabile aggiungendo la parolina magica rec (che serve al compilatore a capire che si sta usando la funzione che si sta dichiarando):
if then else bool = int int n 0 int 1 int n factorial int * int
int
Int
int
Programmazione funzionale NB: Consigliamo di provare qualche altro classic esempio come la sommatoria o fibonacci.
13
In certi casi, nei quali utilizziamo una funzione allinterno di un'altra, che viene richiamata solo da essa, consigliato dichiararla come funzione locale:
let fib n = let rec fib a b n = if n = 0 then a else if n = 1 then b else fib b (a+b) (n-1) in fib 1 1 n;;
con scheletro: val fib : int -> int = <fun>. In pratica la funzione fib locale a fib, fib lo stato di partenza della ricorsione di fib. La si chiama la prima volta passandogli 1 1 n, dove n il parametro di fib.La ricorsione si ferma se n pari a zero oppure se pari ad uno.
PATTERN MATCHING
Il pattern matching un costrutto molto simile allo switch del c++. La sua sintassi :
match expr with pattern1 -> espressione1 | pattern2 -> espressione2 | pattern3 -> espressione3 ...
In pratica gli si dice di confrontare (match) lespressione (expr) con (with) i pattern. Non confronta il valore assoluto bens la similitudine col pattern. pi facile capirlo tramite un esempio:
14
Programmazione funzionale
In pratica, se n 0 ritorna 0, in tutti gli altri casi ritorna n+1, notate la differenza dellesempio precedente in cui n veniva legata ad x. Un altro esempio per capire meglio il funzionamento di questo costrutto:
let rec fib n = match n with 0 | 1 -> 1 | _ -> fib (n-1) + fib (n-2);;
Si confronta n con 0 oppure 1, se uno di questi due valori ritorna 1 altrimenti ritorna sempre un intero, ricavato dalla chiamata ricorsiva alla funzione. importante notare che in tutti i casi di ritorno, si ritorna sempre lo stesso tipo di valore!
LISTE
Una lista una sequenza di lunghezza arbitraria di valori. Ad esempio:
[1; 2; 3];;
Questa una lista di tre elementi di tipo intero. Gli elementi della lista devono avere lo stesso tipo. Possiamo realizzare liste di qualunque tipo (anche di strutture o di altre liste), purch esse siano omogene (ossia contengano in tutto il loro corpo lo stesso tipo).Si possono fare diverse operazioni sulle liste fra cui: cons: che serve per aggiungere un elemento allinizio della lista oppure nei pattern serve per dividere il primo elemento della lista dalla lista stessa (verr spiegato meglio dopo tranquilli). Per fare questo si usa il simbolo ::. Ad esempio:
1 :: [2; 3];;
Ha come risultato una lista composta da tre interi: [1; 2; 3]. Possiamo dire infine che cons una funzione che ha i seguenti tipi:
Richiede un elemento del tipo della lista passata come secondo parametro e restituisce una lista con il primo paramentro in testa alla lista passata come secondo parametro. Concatenazione: serve per concatenare due liste, mettere la seconda lista attaccata allultimo elemento della prima. Il simbolo utiliazzato @. Ad esempio:
[1; 2; 2; 3];;
Programmazione funzionale
15
Richiede quindi due liste dello stesso tipo e restituisce una lista del tipo delle due passate come parametro. Una sintassi che richiede particolare attenzione la seguente. Scrivere
[1; 2; 3]
Equivale a scrivere
1 :: 2 :: 3 :: []
Prestate particolare attenzione perch il simbolo *+ significa lista vuota (servir in seguito). In pratica si dice che esiste una lista di tre elementi, il primo dei quali 1, seguito da 2, seguito da 3 ed il resto della lista vuoto.
1) [] 2) x::xs 3) x::y::[]
Nel primo caso confrontiamo con una lista vuota, nel secondo caso verifichiamo se contiene almeno una elemento, nel terzo caso verifichiamo se ha esattamente due elementi. Col seguente esempio, verranno chiariti i dubbi. Definiamo una funzione che somma tutti gli elementi di una lista di interi, se gli si passa una lista vuota, essa restituisce 0:
let rec sum ls = match ls with [] > 0 | x::xs -> x + sum xs;;
Il nostro parametro ls tipizzato ad una lista di interi perch viene utilizzato loperatore +. Se la lista vuota, allora ritorna 0, altrimenti, se la lista ha almeno un elemento, ritorna quellelemento, sommato al resto della lista, nel caso fosse vuoto, al giro successivo verrebbe ritornato 0. Spieghiamolo con uno schema: Passiamo ad esempio la lista [1; 3; 5]. Il primo passo vedere se vuota, vuota? No, allora non matcha col pattern *+ e confronta la lista passata col pattern successivo. La lista assomiglia ad x::xs? Si perch abbiamo 1::2::3 (se avessimo scritto x::xs::[] non avrebbe metchato, perch prende xs come un intero e non come il resto della lista, dovremmo aver avuto una lista [1; 2]) e quindi matcha su quel pattern, cosa deve fare quel pattern? Deve sommare ad x la chiamata ricorsiva su xs (il resto della lista, di tipo int list). Step to step:
16 sum sum sum sum [1; 2; Ritorna [2; 3] ritorna [3] ritorna [] ritorna 3]
Programmazione funzionale
vuota? No, assomiglia ad x::xs? Si (xs tutta la lista tranne il 1 elemento)
1 + sum [2; 3] vuota? No, assomiglia ad x::xs? Si 2 + sum [3] vuota? No, assomiglia ad x::xs? Si perch 3::[] 3 + sum [] vuota? Si 0 leggi da qui
Ripercorriamo dal basso allalto: 0 + 3 + 2 + 1 = 6 ed esattamente la somma del contenuto della lista. Questo schema rappresenta il funzionamento dello stack. Ricordiamo che anche nei casi delle liste consentituo utlilizzare _, spiegato precedentemente, consigliamo di usarlo per crearsi come esercizio una funzione che conti la lunghezza di una lista qualsiasi. In classe abbiamo fatto diverse funzioni sulle liste, fra cui: length: ritorna la lunghezza di un a list. reverse: data un a list la restituisce al contrario. append: concatena due a list. Consigliamo di provare a rifare queste semplici funzioncine per vedere se si capito fin ora, perch fra poco si far complicata la questione. Ci sono gia implementate sulle diapositive del prof, ma provate a farle senza sbirciare, se proprio non ce la fate aiutatevi con le diapositive. Esempio interessante quello di contare quanti numeri positivi ci sono in una lista:
let rec positive ls = match ls with [] -> 0 | x::xs -> (if x > 0 then 1 else 0) + positive xs;;
Qui si usa il match assieme allif (rivedersi lif se non capite perch dopo si pu usare il + senza problemi). Se ls (il parametro) vuota, allora ritorna 0, altrimenti se il primo elmento maggiore di 0 ritorna 1 sommato alla chiamata ricorsiva sul resto della lista, altrimenti ritorna 0 sommato alla chiamata ricorsiva sul resto della lista. Da questo esempio, ricaviamo una piccola variante: data una lista, costruiscine un altra composta solo dai numeri positivi contenuti in quella passata:
Se ls vuota allora ritorna una lista vuota, altrimenti, estrai il primo element, se esso positive lo si concatena con la chiamata ricorsiva al resto della lista, altrimenti, ri richiama la funzione sul resto della lista senza concatenazione.
Programmazione funzionale
17
Notate come con le chiamate ricorsive sul resto della funzione si possa scorrere una lista senza cicli. Per questo importante che abbiate capito a cosa serve x::xs.
TAIL RECURSION
Lo stack richiesto per computare la somma di n elementi pari a n+1 elementi. Noi in ocaml possiamo usare n relativamente piccoli. Linterprete pu riconoscere una forma particolare di ricorsione chiamata appunto tail recursion, in modo da ottimizzarla, trasformandola di fatto in un ciclo. Per riconoscere se una funzione tail recursive basta osservare se lultima istruzione una chiamata ricorsiva a se stessa. Ad esempio:
let sum n = let rec sum s n = if n = 0 then s else sum (n+s) (n-1) in sum 0 n;;
Si utilizza una funzione locale ricorsiva (rivedersela se non si sa cosa sia). Il primo parametro lo usiamo come accomulatore del risultato, infatti quando il secondo parametro (quello passato alla funzione principale) arriva a 0 si ritorna laccomulatore s.
ACCUMULATORI
Come spiegato sopra, sono molto comodi per realizzare la tail recursion. Permettono di utilizzare una area di memoria (stack) costante. Sfortunatamente non c un modo semplice per linterprete di dirci quando lottimizzazione funziona e quando no. Se non si sicuri, si pu provare la funzione con valori di input elevati. Sulle diapositive, nella lezione 4, dalla pagina 33 in poi ci sono tutte le funzioni ricorsive gia fatte in precedenza, solo che messe in tail recursion. Vi consiglio di provare a farle perch abbastanza complicato capire questa tecnica senza fare esercizi.
18
Programmazione funzionale
let rec sum xs = match xs with [] -> 0 |x::xs -> x + sum xs;;
In questo caso, il parametro xs una variabile diversa da tutti gli xs che si trovano nei pattern. I distruttori, a cui non abbiamo fatto riferimento, vengono nella maggior parte dei casi gestiti implicitamente nel pattern matching. Nel pattern matching, nella maggior parte dei casi esiste un pattern che gestisce il caso base.
POLIMORFISMO
Partiamo subito con un esempio:
let id x = x;;
Cosa fa questa funzione? Dato un qualsiasi x, lo ritorna. Funzione inutile e stupida, per se vi accorgete lavora con qualsiasi tipo di parametro, questo il principio base del polimorfismo. Per capire: id : int x : int
Possiamo quindi concludere che pur chiamandosi nello stesso modo, id faccia compiti diversi in base ai parametri passati. In generale possiamo dire che id sia: id : a x : a
Programmazione funzionale
19
Come gi detto in precedenza, in OCAML si utilizza la sintassi a, b ecc per indicare un tipo arbitrario, in pratica per non specificare alcun tipo. Altro esempio per capire meglio il polimorfismo:
let rec member x xs = match xs with [] -> false | y::ys -> if x = y then true else member x ys;;
Il parametro x un tipo qualsiasi, il parametro xs una lista di tipi uguali al tipo di x. La lista viene tipizzata al tipo di x quando si usa il cons. La funzione non fa altro che verificare che x sia presente nella lista xs. a -> a list -> bool. Per semplificarci le cose ci viene in aiuto il costrutto when, facciamo un esempio e poi spieghiamoli:
let rec member x xs = match xs with [] -> false | y::_ when x = y -> true | _::ys -> member x ys;;
I pattern possono avere una sintassi estesa:
20
Programmazione funzionale
let sign x = match x with 0 -> 0 |x when x > 0 -> 1 |x when x < 0 -> -1;;
Qui vengono esaminati tutti i casi, quindi non si hanno punti ostici. Sulle slide segue lesempio dellordinamento di una lista, guardatevelo per capire meglio quanto fatto fin ora, comunque esiste gia una funzione di ibreria predefinita List.sort con parametro a list che fa questo lavoro.
let rec add1 ls = match ls with [] -> [] | x::xs -> x+1 :: add1 xs;; let rec positive ls = match ls with [] -> [] | x::xs -> x>0 :: positive xs;;
Programmazione funzionale
21
Notiamo con facilit che le due funzioni sono praticamente identiche, cambia solo la condizione di inserimento e il tipo restituito. Diremo quindi che le due funzioni in questione possono essere generalizzate come:
let rec funzione ls = match ls with [] -> [] | x::xs -> fai qualcosa :: funzione xs;;
Al posto di fai qualcosa possiamo mettere qualsiasi cosa, ad esempio mettere un controllo per eliminare i numeri negativi ecc. Siamo cercando di costruire una funzione che abbia come parametri: una funzione che viene applicata a tutti gli elementi di un a list (il nostro fai qualcosa) a list
let rec map funzione lista = match lista with [] -> [] | x::xs -> funzione x :: map funzione lista;;
I tipi di questa funziuone saranno:
22
Programmazione funzionale
e quindi: match ls with [] -> [] | x::xs -> x+1 :: map (x+1) ls;;
Map per fa qualcosa in base alla funzione che gli si passa, quindi possiamo usarla anche in questo modo:
E nello specifico: match ls with [] -> [] | x::xs -> x>0 :: map (x>0) ls;;
Abbiamo quindi visto che possiamo far fare ad una funzione lavori nettamente diversi senza doverla ridichiarare ogni volta. Map diremo che una funzione generalizzata. Ci sono molte altre funzioni che possono essere generalizzate, come ad esempio la somma degli elementi di una lista di interi, il concatenamento di lista di liste, determinare se tutti gli elementi di una lista di booleani sono veri ecc. Sull slide ti fa vedere come si fa, se avete curiosit andate a vedere.
23
let rec fold_left f x xs = match xs with [] -> x | y::ys -> fold_left f (f x y) ys;;
parametri, una funzione f, un valore x di qualsiasi tipo ed una qualsiasi lista xs. Se la lista xs vuota ritorna x, altrimenti fai il pattern matching con y::ys e richiama se stessa passandogli come primo parametro nuovamente la funzione, come secondo parametro il valore che si ottiene applicando tale funzione su x e y, come terzo parametro il resto della lista. Possiamo dire che il secondo parametro assomigli quasi ad un accomulatore. Un esempio di uso di fold_left:
Al suo interno essa richiama la fold_left in questo modo: fold left (+) 0 [5;2;8]
Al suo interno lavorer in questo modo: match [5;2;8] with [] -> x | 5::[2;8] -> fold_left (+) ((+) 0 5) [2;8];;
Quindi al prossimo passo ricorsivo avremo: match [2;8] with [] -> x | 2::[8] -> fold_left (+) ((+) 5 2) [8];;
Abbiamo messo ((+) 5 2) perch 5 il risultato della chiamata ricorsiva precedente, concludiamo tutto il ciclo per vedere bene il comportamento: match [8] with [] -> x | 8::[] -> fold_left (+) ((+) 7 8) [];;
Programmazione funzionale
Alla fine ritorna x, che sar 7+8 e quindi 15. Provare per credere. Quindi abbiamo visto un altro esempio di funzione generalizzata. Possiamo fargli fare lavori interessanti alla fold left, come quello di controllare se tutti i membri di una lista di booleani sono veri:
let rec filter f ls = match ls with [] -> [] | x::xs when f x -> x :: filter f xs | _::xs -> filter f xs;; Val filter : (a > bool) -> a list -> alist = <fun>
Filter prende come parametric una funzione che restituisce un booleano ed una lista. Si vede che la funzione restituisce un booleano perch viene utilizzata come condizione nel when. Se la lista vuota ritorna la lista vuota, se la lista ha degli elementi che soddisfano la funzione passata come parametro gli inserisce in cima ala lista richiamando ricorsivamente se stessa sul resto della lista, altrimenti fa la richiamata ricorsiva senza inserire lelemento in cima alla lista, di fatto scartandolo ed esaminando il resto della lista. facile capire che la funzione che gli si passa la condizione di inserimento (degli elementi della lista passata come parametro) nella lista di ritorno. Nelle slide ci sono vari esempi sulluso di filter, come quello dei numeri positivi, di quelli che non sono zero ecc, per curiosit guardateli, ma se avete capito bene fin qui, non serve guardarli. NB: la map, fold left e filter sono funzioni gia predefinite, basta richiamarle mettendovicisi un List. davanti.
Programmazione funzionale
25
FUNZIONI ANONIME
Alcune volte le funzioni vengono usate una volta soltanto, per questo il loro nome non ha molta importanza. Queste funzioni vengono dette anonime e hanno la seguente sintassi:
fun x -> x + 1
Per utilizzarla scriveremo ad esempio:
Ricordiamo che (+) la sintassi alternativa di +, invece che prendere un parametro sinistro ed uno destro, li prende tutti e due a destra. Un esempio di applicazione parziale delle funzioni potrebbe essere:
26
Programmazione funzionale
secondo parametro
terzo parametro
ritorno
Esempio
5 :: [6; 7] [5] @ [6; 7] List.length [5; 6; 7] List.hd [3; 5; 7] List.tl [3; 5; 7] List.nth [3; 5; 7] 2 List.rev [1; 2; 3]
Note
Attacca un element in cima alla lista. Concatena due liste Ritorna il numero degli elementi di una lista Ritorna lelemento in testa della lista Ritorna la lista senza il primo elemento Ritorna lennesimo elemento della lista partendo da zero Rovescia la lista Applica una funzione a tutti gli elementi della lista Piega verso sinistra Applica il filtro passato alla lista
List. map: (a -> b) -> a list List.map ((+)1) [1;2] -> b list List.fold_left (a -> b -> a) List.fold_left (+) 0 [1;2] a -> b list -> a List.filter (a -> bool) -> a list -> a list List.sort (a -> a -> int) -> a list -> a list List.filter ((<) 0) [1; -2]
List.sort compare [1; -2; 3; -4] Ordina una lista in base alla funzione che gli si passa
Programmazione funzionale
27
DEFINIZIONE DI TIPI
Molte volte abbiamo necessit di dichiarare dei nostri tipi, che possono contenere svariati valori differenti:
let string_of_seme s = match s with Cuori -> Cuori | Picche -> Picche | Quadri -> Quadri | Fiori -> Fiori;;
possibile costruire altri tipi, utilizzando I tipi da noi costruiti:
let string_of_carta (Carta (s,v)) = match v with 1 -> Asso di ^ string_of_seme s | 11 -> Jack di ^ string_of_seme s | 12 -> Regina di ^ string_of_seme s | 13 -> Re di ^ string_of_seme s | _ -> string_of_int v ^ of ^ string_of_seme s;;
La sintassi generale :
type nome =
28
Programmazione funzionale
ALBERI
Un albero un insiemi di nodi, tutti con un padre in relazione ad ognuno di loro come P(n,m). Esiste solo un nodo di root (il padre dei padri, chiamatelo anche DIO), che il primo nodo dellalbero, quello che non ha padri. Ogni nodo, apparte il root ha un solo padre. Per ogni ennesimo nodo, apparte il root, esiste un percorso che collega quellennesimo nodo al root. Se noi abbiamo un albero formato da n1 ... nk nodi dove k diverso da 1, possiamo dire che n1 il root dellalbero, per ogni nodo da 1 a k-1, ni il padre di ni+1. La rappresentazione grafica degli alberi quella che si sempre vista fin ora:
root
nodo interno
nodo interno
nodo interno
foglia
foglia
foglia
foglia
Un albbero con un singolo nodo root r senza padri, un albero con root r. Se t 1, t2, ... tn sono nodi con root r1, r2, ... rn e r un nuovo nodo, allora la srtuttura ottenuta data dallaggiunta di r come padre di t1, t2, ... tn. Parlando di alberi, useremo la seguente terminologia: Path: la sequenza di nodi n1, ... nk, nk+1 che viene attraversata per arrivare da n1 a nk. In parole povere il percorso che sta fra un nodo ed un altro nodo che stiamo esaminando. Lunghezza del path: il numero di nodi meno unp che si contano nel path di un nodo. Antenato: se esiste un percorso (path) da m a n allora m lantenato di n. Discendente: se esiste un percorso (path) da m a n allora n il discendente di m. Fratelli: nodi con lo stesso padre. Sottoalbero: immaginiamo di essere in un nodo di un albero che non la root, immaginiamo che quel nodo sia la root (quindi dimenticandoci tutto ci che sta a sopra e affianco di quel nodo) quello che rimane il sottoalbero di n. Foglia: nodo senza figli. Nodo interno: nodo con uno o pi figli. Altezza di un nodo: lunghezza del path pi lungo ad una foglia. Altezza di un albero: altezza del root . Profondit di un nodo: lunghezza del path di quel nodo dalla root. Dimensione di un albero: numero di nodi che quellalbero possiede.
Programmazione funzionale
29
ALBERI BINARI
Un albero binario un albero in cui ogni nodo ha al pi due figli. Un albero binario pu essere vuoto, se t1 e t2 sono alberi binari, allora anche Tr(t1,t2) un albero binario. Spieghiamo meglio questa affermazione: Prendiamo t1 t2
Sono evidentemente alberi binari, se noi mettiamo un padre in comune a t1 e t2, avremo comunque un albero binario:
t1
t2
type a tree =
In pratica un a tree pu essere un Emty (vuoto) oppure una struttura che ha un a e due a tree: possiede uno spazio spazio per memorizzare un dato qualsiasi, e poi due possibili figli, che sono a loro volta un a tre, quindi possono essere vuoti o possedere un a, e due a tree, possiamo andare avanti a piacimento. La parola chiave Tr verr utilizzata come costruttore di nodi, essa richieder 3 parametri, un a (che il valore che verr memorizzato nel nodo), un a tree (che il figlio sinistro) ed un altro a tree (che il figlio destro). Un albero che scritto in OCAML fatto in questo modo:
Tr(1, Tr (2, Tr (4, Empty, Empty), Tr (5, Empty, Empty)), Tr (3, Tr (6, Empty, Empty), Empty));;
Programmazione funzionale
Il primo tr Tr(1
Costruisce un nodo nel quale, nel campo a memorizzato 1, tipizzando lalbero a a tree. Il costruttore per si aspetta altri 2 parametri: 2 a tree, essi possono essere nodi veri e propri (quindi dei nuovi Tr) oppure degli Empty, per definizione del tipo a tree sescritta in precedenza. Scrivere quindi Tr(1 , Empty, Empty)
Al posto di Empty possiamo scrivere altri Tr, lampante quindi che gli alberi sono strutture ricorsive. Abbiamo costruito anche delle funzioni per capire su che tipo di nodo stiamo lavorando. Per fare questo ci siamo serviti di un eccezione. Il concetto di eccezione in OCAML lo stesso degli altri linguaggi.
exeption EmptyTree;;
Piccola parentesi per spiegare meglio le eccezioni. Le eccezioni sono degli errori, ad esempio 1/0 solleva leccezione Division_by_zero. Le eccezioni sono sollevate quando non possiamo computare un valore. Esistono diverse eccezioni predefinite per differenti tipi di errori. Le eccezioni possono ssere create con la sintassi: try espressione with pattern_espressione -> espressione pattern_espressione -> espressione ...
Programmazione funzionale
31
Dopo aver dichiarato leccezione, la rpima funzione che andremo ad analizzare si chiama root. Essa dato un albero, fornisce il contenuto del root, se non esiste un rooot allora lalbero vuoto e quindi solleva leccazione precedentemente dichiarata.
Un altra funzione la left essa dato un nodo, fornisce il sottoalbero sinistro di quel nodo. Se non esiste un figlio sinistro, solleva leccezione dichiarata.
Se noi ad esempio dichiarassimo questo albero: let albero = Tr(1, Tr(2, Tr(4, Empty, Empty), Tr(5, Empty, Empty)), Tr(3, Tr(6, Empty, Empty), Empty));;
Essa fornirebbe come risultato: - : int tree = Tr (2, Tr (4, Empty, Empty), Tr (5, Empty, Empty))
Programmazione funzionale
Un altra funzione la right che fa lo stesso lavoro della left, solo che invece di ritornare il sottoalbero sinistro, ritorna quello destro, se non esiste un sottoalbero destro solleva leccezione.
Ulteriore funzione la is_empty. Dato un albero dice se vuoto oppure no. Quindi ritorna un booleano.
Funzione molto comoda la leaf. Essa richiede un a e ritorna un a tree. In pratica, inserisce il parametro che gli passate in un nodo che non ha figli. (si chiama leaf perch di fatto crea una foglia).
La is_leaf dice se un nodo che gli passiamo come parametro una foglia oppure no, ritornando un booleano.
let is_leaf x = match x with Tr(_, Empty, Empty) -> true | _ -> false;;
val is_leaf : 'a tree -> bool = <fun>
let rec size t = match t with Empty -> 0 | Tr(_, t1, t2) -> 1 + size t1 + size t2;;
val size : 'a tree -> int = <fun>
Programmazione funzionale
33
una funzione ricorsiva diversa da quelle che abbiamo visto fin ora. Le liste erano strutture semplici dato che come elemento successivo avevano un solo nodo, gli alberi binari invece come elemento successivo possono avere 2 nodi, quindi quando si vuole fare delle operazioni su un albero, scorrendolo, bisogna andare in ricorsione sia sul figlio destro che su quello sinistro. Credo che a questo punto sia chiaro il funzionamento della size, ma se non si capito, andatevi a vedere il capitolo della ricorsione e del pattern matching. La reflect , dato un albero, lo specchia, in pratica gira a destra tutto ci che a sinistra e viceversa.
let rec reflect t = match t with Empty -> Empty | Tr(x, t1, t2) -> Tr(x, reflect t2, reflect t1);;
val reflect : 'a tree -> 'a tree = <fun>
La height dato un albero, ne calcola la profondit massima (la distanza massima fra una delle foglie e il root).
let rec height t = match t with Empty -> -1 | Tr(_, t1, t2) -> 1 + max (height t1) (height t2);;
val height : 'a tree -> int = <fun>
La max una funzione predefinita che ritorna il Massimo fra due interi. Non credo sia difficile capire come faccia a calcolare la profondit, ma se non fosse chiaro vi consiglio di prendere un albero di esempio e fare passo per passo cosa farebbe la funzione. La fulltree, dato un intero n restituisce un albero che ha n livelli, e li riempie con dei numeri.
let fulltree n = let rec fulltree_ k n = if n <= -1 then Empty else Tr(k, fulltree_ (2*k) (n-1), fulltree_ (2*k +1) (n-1)) in fulltree_ 1 n;;
val fulltree : int -> int tree = <fun>
Programmazione funzionale
1 Livello 0 Livello 1 2 3
4 Livello 2
La treeprint stampa un albetro in maniera pi adatta alla lettura. Gli si deve passare una funzione di stampa ed un albero. Ad esempio se lalbero intero gli si passer la print_int, se un albero di bool gli si passer la print_bool ecc.
let treeprint print t = let rec aux ind t = match t with Empty -> print_string(ind ^ "Empty") | Tr(x, Empty, Empty) -> print_string (ind ^ "Tr("); print x; print_string ", Empty, Empty)" ; | Tr(x, t1, t2) -> print_string (ind ^ "Tr("); print x; print_string ",\n" ; aux (" " ^ ind) t1; print_string ",\n" ; aux (" " ^ ind) t2; print_string ")" in aux "" t; print_string "\n" ;;
val treeprint : ('a -> 'b) -> 'a tree -> unit = <fun>
Il risultato sul nostro famoso albero dichiarato precedentemente sar: treeprint print_int albero;; Tr(1, Tr(2, Tr(4, Empty, Empty), Tr(5, Empty, Empty)), Tr(3, Tr(6, Empty, Empty), Empty)) - : unit = ()
Programmazione funzionale
35
Let rec balanced t = match t with Empty -> true | Tr(_, t1, t2) -> balanced t1 && balanced t2 && abs (height t1 height t2) <=1;;
val balanced : a tree -> bool = <fun>
In pratica per ogni nodo, fa land della chiamata ricorsiva sul sottoalbero sinistro e quello destro assieme al risultato del confronto fra il valore assoluto (abs) della profondit del sottoalbero sinisro meno quello destro che sia minore o uguale di 1.
Facendo: balanced (Tr(1, Tr(2, Tr(3, Tr(4, Empty, Empty), Empty), Empty), Empty)) ;;
Otterremo che la funzione passo per passo faccia: balanced <-- Tr(1, Tr(2, Tr(3, Tr(4, Empty, Empty), Empty), Empty), Empty) balanced <-- Tr(2, Tr(3, Tr(4, Empty, Empty), Empty), Empty) balanced <-- Tr(3, Tr(4, Empty, Empty), Empty) balanced <-- Tr (4, Empty, Empty) balanced <-- Empty balanced --> true balanced <-- Empty balanced --> true balanced --> true balanced <-- Empty balanced --> true balanced --> true balanced <-- Empty balanced --> true balanced --> false balanced --> false
Finch analizza degli empty restituisce sempre true, quando invece arriva a dei nodi che hanno figli, deve fare il confronto anche fra le due profondit dei figli, ritornado true oppure false. chiaro che se si ritorna anche un solo false, tutta la faccenda sar false (propriet delland). Facendo land fra tutti questi risultati abbiamo come risultato finale: - : bool = false
36
Programmazione funzionale
Il problema di questa funzione che ogni sottoalbero attraversato due volte, una volta con la chiamata ricorsiva sui figli destri e sinistri, ed una volta con la chiamata alla height. Lottimizzazione di questo algoritmo si pu ottenere ritornando la profondit quando bilanciato, un eccezione quando non lo . Dichiariamo quindi leccezione che serve a noi:
exception NotBalanced;;
Ed ottimizziamo il codice scritto sopra:
let balanced_opt t = let rec balanced_ t = match t with Empty -> -1 | Tr(_, t1, t2) -> let k1 = balanced_ t1 in let k2 = balanced_ t2 in if abs(k1 - k2) <= 1 then 1 + max k1 k2 else raise NotBalanced in try let _ = balanced_ t in true with NotBalanced -> false ;;
Qui oltre che funzioni locali, vengono usate variabili locali. Questa funzione difficile da spiegare a parole, pertanto baster fare passo per passo cosa farebbe la funzione applicata ad un caso specifico. Esistono vari metodi per visitare un albero binario: Visitando ciascun nodo dellalbero in qualche ordine per qualche scopo Con tre tipi di algoritmi diversi: o Preorder: analizza elemento; preorder figlio sinistro; preorder figlio destro; o Inorder: inorder figlio sinistro; analizza elemento; inorder figlio destro; o Postotder: postorder figlio sinistro; postorder figlio destro, analizza figlio
In seguito verranno riportati gli esempi di ciascun algoritmo, tutti devono comunque mettere un albero in una lista. Lesempio di preorder :
let rec preorder t = match t with Empty -> [] | Tr(x, t1, t2) -> x :: (preorder t1 @ preorder t2);;
Se facciamo preorder (fulltree 2);;
37
Lesempio di inorder :
let rec inorder t = match t with Empty -> [] | Tr(x, t1, t2) -> (inorder t1) @ (x :: (inorder t2)) ;;
Applicandolo sul fulltre 2 avremo: inorder (fulltree 2);; - : int list = [4; 2; 5; 1; 6; 3; 7]
Quindi inserisce nella lista nellordine in cui compaiono nellalbero partendo dal basso a sinistra. Infine il post order
let rec postorder t = match t with Empty -> [] | Tr(x, t1, t2) -> (postorder t1) @ ((postorder t2) @ [x]) ;;
Applicandolo sul nostro fulltree 2: postorder (fulltree 2);; - : int list = [4; 5; 2; 6; 7; 3; 1]
Si pu quindi notare in maniera lampante che tutte e tre le funzioni solo del tipo: :- a tree -> a list = <fun>
38
Programmazione funzionale
let rec take l n = match l with [] -> [] | _::_ when (n <= 0) -> [] | x::xs -> x:: take xs (n 1);;
val take : 'a list -> int -> 'a list = <fun>
Questa, presa una lista, ne ritorna una con n elementi della prima. take [1;2;3;4] 2;; - : int list = [1; 2]
La seconda la drop:
let rec drop l n = match l with [] -> [] | _::_ when (n <= 0) -> l | x::xs -> drop xs (n 1);;
val drop : 'a list -> int -> 'a list = <fun>
Presa una lista ne ritorna un altra con gli elementi di quella passata, tranne I primi n. drop [1;2;3;4] 2;; - : int list = [3; 4]
let rec balpreorder l = match l with [] -> Empty | x::xs -> let k = List.length xs / 2 in Tr(x, balpreorder (take xs k), balpreorder (drop xs k));;
val balpreorder : 'a list -> 'a tree = <fun>
Programmazione funzionale
39
Data una lista costruisce un albero binario bilanciato, lo fa andando in ricorsione del figlio sinistro con la parte sinistra della lista, del figlio destro con la parte destra della lista. Ad esempio: let albero = balpreorder [1;2;3;4;5;6;7];; val albero : int tree = Tr (1, Tr (2, Tr (3, Empty, Empty), Tr (4, Empty, Empty)), Tr (5, Tr (6, Empty, Empty), Tr (7, Empty, Empty)))
Ed chiaramente bilanciato. Possiamo anche costruire un albero bilanciato con la maniera inorder:
let rec balinorder l = match l with [] -> Empty | xs -> let k = List.length xs / 2 in let y::ys = drop xs k in Tr(y, balinorder (take xs k), balinorder ys;;
val balinorder : 'a list -> 'a tree = <fun>
balinorder [1;2;3;4;5;6;7];; - : int tree = Tr (4, Tr (2, Tr (1, Empty, Empty), Tr (3, Empty, Empty)), Tr (6, Tr (5, Empty, Empty), Tr (7, Empty, Empty))) 4
40
Programmazione funzionale
Per fare questa funzione useremo un accumulatore (spiegato nei capitoli precedenti). Le intuizioni che possiamo fare riguardo a questo lavoro sono che il caso base, lalbero vuoto, ritorni un valore parziale. Nel caso generico, quando abbiamo un nodo Tr(x, t1, t2), otteniamo un nuovo risultato, aggiungendo il risultato su x al risultato precedente, visitiamo t1, otteniamo un risultato, visitiamo t1 e ritorniamo un risultato. Costruiamo una funzione che controlli dove inserire un elemento di un nodo in una lista di tuple come descritto sopra:
let rec addto pairlist y = match pairlist with [] -> [(y,1)] | (x,n)::[]rest when x = y -> (x,n+1)::rest | (x,n)::rest -> (x,n)::(addto rest y);;
Questa funzione quindi data una lista di tuple ed un element che ha lo stesso tipo del primo elemento di ciascuna tupladella lista, cerca se quellelemento gia stato inserito nella lista, se si allora incrementa il contatore di quellelemento, altrimenti crea una nuova tupla infondo alla lista con quellelemento ed il suo contatore ad 1. Ora creiamo la funzione che faccia il lavoro principale:
let count t = let rec count_ t res = match t with Empty -> res | Tr(a, t1, t2) -> count_ t2 (count_ t1 (addto res a)) In count_ t [];;
Programmazione funzionale
41
In pratica su ogni nodo richiama la addto dichiarata precedentemente, passandogli lelemento del nodo attuale e laccumulatore (la lista di tuple), la prima volt ache si incontra quellelemento verr create una tupla da mettere nella lista, tutte le alter volte bisogna ricercare quellelemento nella lista ed incrementare il contatore. Questo lavoro, come abbiamo gia visto lo fa la addto. Esistono anche degli algoritmi pi efficienti fatti con la preorder, inorder e postorder analizzeremo. che non
let rec treemap f t = match t with Empty -> Empty | Tr(x; t1; t2) -> Tr (f x, treemap f t1, treemap f t2);;
val treemap: (a -> b) -> a tree -> b tree = <fun>
Come nella List.map, viene applicata la funzione agli elementi di ogni nodo, andando in ricorsione sul figlio destroy e sinistro. praticamente identica alla List.map solo che lavora su un albero invece che su una lista. Ad esempio se noi facciamo: let albero = Tr(1, Tr(2, Tr(4, Empty, Empty), Tr(5, Empty, Empty)), Tr(3, Tr(6, Empty, Empty), Empty));; treemap (fun x ->( x + 100)) albero;;
Otterremo un albero con tutti gli elementi di albero dommati a 100: let albero = Tr(101, Tr(102, Tr(104, Empty, Empty), Tr10(5, Empty, Empty)), Tr(103, Tr(106, Empty, Empty), Empty));;
Possiamo fare moltissime variabili di questa cosa, ma mi sembra inutile se si capito la List.map.
42
Programmazione funzionale
DJ VU
Apro una piccola parentesi per tranquillizzare I lettori. Sapete che cos un dj vu? quella sensazione di aver gi vissuto una situazione. Sapete perch succede questo?
Noi tutti ci immaginiamo la nostra vita come una retta che cresce man mano nel tempo, infatti, dal nostro punto di vista la nostra vita appare lineare:
In realt non proprio cos: ogni volta che ci troviamo davanti una scelta, la linea si divide, in base a quante opzioni abbiamo per quella scelta:
Il dj vu non altro che un momentaneo sguardo su un altro ramo della nostra vita. In effetti ci sembra di avere gia visuto quella scena perch noi labbiamo vissuta, ma in un altra dimensione. Esistono diverse versioni di ogni cosa. Non centra nulla con programmazione funzionale, ma almeno c un albero . Chiudiamo in bellezza la parentesi.
Programmazione funzionale
43
ESAMINARE UNESPRESSIONE
Riprendendoci al capitolo della definizione dei tipi, cerchiamo di rappresentare una seplice espressione aritmetica contentente: Costanti (float) Variabili Addizione Moltiplicazione
type expr =
Constant of float | Variable of string | Sum of (expr * expr) | Mul of (expr * expr);;
Si definisce un tipo chiamato expr che ha quattro campi, uno che contiene delle costanti di tipo float, uno che contiene variabili denominate da stringhe, uno che contiene la somma di due tipi expr (quindi ricorsivo) ed uno che contiene la moltiplicazione fra due expr (ricorsivo).
Costant
Float
Sum
I campi mul e sum sono collegati ad altre due strutture di questo genere. Con questo tipo possiamo quindi rappresentare delle espressioni che contengono una somma oppure una moltiplicazione oppure una costante o variabile. Ogni volta che si usa questo tipo bisogna scegliere fra uno ed uno solo di questi campi. Se si sceglie uno dei campi espressione, bisogna andare avanti a dirgli a che altra espressione sono collegati. Prima di iniziare vediamo come stampare questa struttura:
let rec string_of_expr e = match e with Constant c | Variable v | Sum (e1,e2) | Mul (e1,e2) e1 ^ )
44
Programmazione funzionale
Se il parametro che gli passiamo una costante allora richiama una conversione float to string, se una variabile gia una stringa quindi siamo apposto, se una Sum oppure una Mul deve andare in ricorsione sui loro parametri, che sono a loro volta nuove espressioni (quindi potrebbero essere Costant, Variable, Sum oppure Mul), continuando finch non arriva in uno dei due casi base (Costant o Variable). string_of_expr : expr -> string = <fun>
let e = Sum (Mul (Constant 7., Variable x), (Mul (Constant 3., Variable y));;
Per capire meglio cosa si fatto ecco lalbero di sintassi astratta:
C V
S M
C V
S M
C V
S M
C S V M
C S V M
C S V M
C V
S M
Da Costant e Variable non possono partire altre frecce, infatti nella definizione del tipo si vede che possono contenere dei valori. Da Sum e da Mul invece devono partire delle frecce ad altre expr. Ogni nodo dellalbero astratto di tipo expr. Per rappresentare il binding delle variabili, come ad esempio si era visto nellambiente {x -> 3.}
Noi useremo una lista di tuple (string * float) list. In pratica ogni volta che useremo una variabile, essa verr inserita in questa lista ambiente. Ogni elemento della lista una tupla. La tupla rappresenta il nome della variabile ed il suo valore. Se prendiamo lesempio di ,x -> 3.}, nella nostra lista verr rappresentato come: [(x, 3.)]
Programmazione funzionale Per fare questo dichiariamo per chiarezza una lista vuota che si chiama empty
45
Il parametro m la nostra famosa lista ambiente, il parametro v il nome della variabile, il parametro x il valore che essa assume. Ogni volta che si usa una variabile, essa viene messa in cima a questa lista. Ad esempio vogliamo fare un let x = 1 (non si fa veramente ma solo per capirci):
Gli si ripassa m perch stata riempita precedentemente. Ora vediamo come estrarre delle variabili da questa lista. Ci serviamo di una funzione che ritorna 0. Se non trova la variabile che gli passiamo, mentre se la trova, prende la prima volta che essa stata usata e ritorna il suo valore:
let rec lookup m v = match m with [] -> 0. | (v, x)::_ when v = v -> x | _::ms -> lookup ms v;;
val lookup : (a *float) list -> a -> float = <fun>
Gli si deve passare il primo parametro m che la lista ambiente e il parametro v che il nome della variabile da cercare nella lista. Se lo trova ritorna lelemento della tupla in cui contenuto che corrisponde al suo valore. Se non lo trova va in ricorsione sul resto della lista finch non lo trova oppure finch la lista vuota. In quel caso ritorna 0.
46 Prendendo la lista che abbiamo costruito prima: let m = *(y, 2.); (x, 1.)+;;
Programmazione funzionale
Andr a cercare la tupla dove c x al primo posto, se la trova ritorna il valore di quella tupla al secondo posto. Se non la trova ritorna 0.. In questo caso la trova e ritorna: :- float = 1.
Crea una nuova lista ambiente fatta in questo modo: *(x, 3.);(y, 2.); (x, 1.)+
Funziona proprio come lambiente, si tiene traccia di tutte le assegnazioni e prende in considerazione solo lultima.
VALUTARE UN ESPRESSIONE
Per valutare un espressione, in pratica risolverla, useremo questa funzione:
let rec eval m e = match e with Costant c | Variable v | Sum (e1, e2) | Mul (e1, e2)
-> c -> lookup m v -> (eval m e1) +. (eval m e2) -> (eval m e1) *. (eval m e2);;
val eval : (string * float) list -> expr -> float = <fun>
Gli si passa m che la solita lista ambiente dove cercare il binding delle variabili (se vengono utilizzate) e lespressione e. Si confronta e con una costante, se lo si ritorna quella costante. Si confronta con una variabile, se lo bisogna cercare a cosa legata nella lista m e ritornare il valore a cui legata usando la
Programmazione funzionale
47
lookup. Se somma oppure moltiplicazione bisogna andare a vedere su che due espressioni si lavora, quindi andando in ricorsione su di esse.
let rec lookup m v = match m with [] -> None | (v, x)::_ when v = v -> Some x | _::ms -> lookup ms v;;
Val lookup : (string * float) list -> a -> a option
Se trova la variabile nellambiente ritorna Some e il valore a cui legata, altrimenti None. Dobbiamo essere capaci di valutare espressioni con assegnazioni incomplete. Per fare questo usiamo la funzione op:
let op f x y = match x, y with (Some x', Some y') -> Some (f x' y') | _ -> None;;
val op : ('a -> 'b -> 'c) -> 'a option -> 'b option -> 'c option = <fun>
Programmazione funzionale
E la moltiplicazione come:
let rece val m e = match e with Costant c -> Some c | Variable v -> lookup m v | Sum (e1, e2) -> sum (eval m e1) (eval m e2) | Mul (e1, e2) -> mul (eval m e1) (eval m e2);;
In questo modo riusciamo a valutare anche espressioni in cui non compaiono tutti i parametri, perch in quel caso viene ritornato None.
ALBERI GENERICI
Un albero generico, detto anche ennario, un albero che ha un numero finito di figli, quindi anche pi di due. Come per i binari, Empty un albero ennario. Cosa cambia dagli alberi binari? Vi ricordate comerano fatti? 4
Gli alberi ennari sono praticamente identici, solo che invece di avere un riferimento al figlio sinistro ed un riferimento al figlio destro, hanno una lista di riferimenti ai figli:
49
let leaf x = Tr (x, []);; let rec preorder (Tr(x,tlist)) = x::List.flatten(List.map preorder tlist);;
Dove List.flatten : List.flatten;; - : 'a list list -> 'a list = <fun> List.flatten [[1;2;3];[4;5;6]];; - : int list = [1; 2; 3; 4; 5; 6]
let rec inorder (Tr(x,tlist)) = match tlist with [] -> [x] | t::ts -> inorder t @ [x] @ List.flatten (List.map inorder ts);; Let rec postorder (Tr(x, tlist)) = (List.flatten (List.map postorder tlist)) @ [x];; exeption Maxl;; let rec maxl l = match l with [] -> raise Maxl | [x] -> x | x::xs -> max x (maxl xs);; Let rec height (Tr(x, tlist)) = Match tlist with [] -> 0 | _ -> 1 + maxl (List.map height tlist);;
50
Programmazione funzionale
HASH TABLES
In informatica una hash table, detta anche hash map, in italiano tabella hash una struttura dati usata per mettere in corrispondenza una data chiave con un dato valore. Viene usata per l'implementazione di strutture dati astratte associative come Map o Set. Pu usare qualsiasi tipo di dato come indice. Il tipo dellhash table Type (a, b) t
Lhash table quindi un tipo da a in b. Esistono come per tutte le strutture dati predefinite alcune funzioni primitive. La Hasthtbl.create crea una nuova hash table, con dimensione n e inizializzata a vuota. N solo una dimensione iniziale, lhash table pu crescere a piacimento.
Hashtabl.create n;;
- : int -> (a, b) t = <fun>
Esiste anche la funzione clear che serve per svuotare un hash table:
Hashtbl.clear t;;
-: (a, b) t -> unit = <fun>
La funzione add serve ad aggiungere un riferimento fra x e y nellhash table. I primi riferimenti di x non sono rimossi ma semplicemente nascosti.
Hashtbl.add t x y;;
-: (a, b) t -> a -> b -> unit = <fun>
In pratica se abbiamo una tabella in questo modo: Elemento ciao cane Riferimento 1 2
E facciamo Hashtbl.add t gatto 5, avremo come risultato su di essa: Elemento ciao cane gatto Riferimento 1 2 5
Hashtbl.remove t x;;
-: (a, b) t -> a -> unit = <fun>
Programmazione funzionale Avendo quindi la tabella: Elemento gatto cane gatto Riferimento 1 2 5
51
Hashtbl.copy t;;
-: (a, b) t -> (a, b) t = <fun>
La funzione find, va a cercare in una data hash table il riferimento ad x, se non lo trova, solleva leccezione Not_found:
Hashtbl.find t x;;
-: (a, b) t -> a -> b = <fun>
La find restituisce il primo riferimento e poi si ferma. Per ritornare tutti i i riferimenti ad un elemento si usa la find_all:
Hashtbl.find_all t x;;
-: (a, b) -> a -> b list = <fun>
Programmazione funzionale
La funzione mem ritorna vero se esiste un elmento x in una hash table, altrimenti ritorna falso:
Hashtabl.mem t x;;
-: (a, b) t -> a -> bool = <fun>
La replace come la add, solo che invece di aggiungere un riferimento, modifica lultimo gi esistente:
Hashtbl.replace x y;;
-: (a, b) t -> a -> b -> unit = <fun>
Fare la replace equivale quindi a fare prima una remove di x e poi una add. La funzione iter applica una funzione f a tutti gli elementi della tabella:
Hastbl.iter f t;;
-: (a -> b -> unit) -> (a -> b) t -> unit = <fun>
La f riceve il riferimento come primo argomento, e come secondo riceve lelemento. Ogni riga della tabella presentata una sola volta ad f. Lordine in cui vengono lette le righe per passarle ad f non specificato.
MODULI
Esistono una serie di moduli predefiniti. I moduli contengono varie definizioni. Possiamo immaginare quindi che i moduli siano delle librerie. Noi ad esempio abbiamo gia usato numerosi moduli, come List, Hashtbl ecc. Si pu accedere alla funzioni di un modulo scrivendo: Modulo.funzione
Come noi ad esempio usavamo la List.map, stavamo accedento alla funzione definita col nome di map, nel modulo List.
Programmazione funzionale In seguito riportiamo una lista di moduli predefiniti: Char - contenente operazioni sui caratteri String - contenente operazioni sulle stringhe Sys - contenente operazioni di interfaccia di sistema Random - contenente operazioni inerenti alla generazione di numeri casuali Array - contenente operazioni sugli array Pervasives - contenente operazioni sugli interi e sui float
53
open List;;
Dopo questo comando possiamo riscrivere a piacimento le funzioni nel modulo aperto. Questo comunque non raccomandato. Il modulo Pervasives aperto automaticamente. Anche se le abbiamo gia ampiamente viste, ripostiamo alcune delle funzioni contenute nel modulo List: length hd tl rev map fold_ left filter sort a list -> int a list -> a a list -> a list a list -> a list (a -> b) -> a list -> b list (a -> b -> a) -> a -> b list -> a (a -> bool) -> a list -> a list (a -> a -> int) -> a list -> a list Ritorna la lunghezza della lista Ritorna il primo elemento della lista Ritorna tutta la lista senza il primo elemento Ritorna la lista al rovescio Vedi capitoli precedenti Vedi capitoli precedenti Vedi capitoli precedenti Vedi capitoli precedenti
Il modulo Char invece contiene: code chr lowercase uppercase char -> int int -> char char -> char char -> char Ritorna il valore ascii corrispondente al carattere Converte il codice ascii in carattere Ritorna il carattere minuscolo Ritorna il carattere maiuscolo
Il modulo String contiene: length get sub contains index string -> int string -> int -> char string -> int -> int -> string String -> char -> bool string -> char -> int Ritorna la lunghezza della stringa Ritorna lennesimo carattere Ritorna la sottostringa da n a m Dice se contiene o no un carattere Ritorna lindice del carattere cercato
54
Programmazione funzionale
PROGRAMMI STAND-ALONE
Per fare un eseguibile in Ocaml scrivete un codice del tipo:
Non esiste il main e le espressioni sono risolte durante lesecuzione. Seguono un sacco di cose (secondo noi) tralasciabili al fine del tema, dato che da qui in poi non ha mai messo questi argomenti. Spiega la compilazione e altre balle sugli stand alone.
INPUT OUTPUT
Abbiamo visto finora solo funzioni di output. Come ad esempio la print_char o print_string ecc (capitoli precedenti). Ora vediamo un po di input: read_line read_int read_float unit -> string unit -> int unit -> float Acquisisce da un buffer e ritorna una stringa Acquisisce da un buffer e ritorna un intero Acquisisce da un buffer e ritorna un float
Lidea di base quella di leggere una riga alla volta utilizzando la read line, incrementando un contatore ogni volta che si incontra una nuova riga:
Programmazione funzionale
55
let rec lc n = try let _ = read_line() in lc (n+1);; with End_of_file -> n;;
Poi possiamo fare: Print_int (lc 0) Print_string \n;;
Per stampare tutto in maniera pi elegante. Possiamo anche usare la funzione ignore per ignorare il risultato di readline, per rendere il tutto pi efficiente.
let rec lc n = try ignore (read_line()) lc (n+1);; with End_of_file -> n;;
Li/o su file si basa sulluso di canali: type in_channel tipo per canale di input type out_channel tipo per canale di output Ci sono tre canali predefiniti: stdin stdout stderr
Per apertura e chiusura di canale si usa: open_out Open_in close_out close_in String -> out_channel string -> in_channel out_channel -> unit in_channel -> unit Apre in scrittura un canale Apre in lettura un canale Chiude un canale in scrittura Chiude un canale in lettura
56 Per scrivere su un canale: output_char output_string out_channel -> char -> unit out_channel -> string -> unit
Programmazione funzionale
Scrive un carattere dove sta puntando il file Scrive una stringa dove sta puntando il file
Per leggere da un canale input_char input_line in_channel -> char in_channel -> string Legge un carattere dove sta puntando il file Legge una stringa dove sta puntando il file
I parametri per la linea di comando, similmente al c sono inseriti in Sys.argv. Il valore di questo dato un array che pu essere convertito in una lista con Array.to_list. Il primo elemento il nome del comando, tutti gli altri sono gli argomenti. Ora creiamo una funzione che conta i caratteri di un canale:
let rec count_channel ch n = try ignore (input_char ch); count_channel ch (n+1) with End_of_file -> n;;
Se non si alla fine del file, si estrae un carattere da un file, ignorandolo, ma incrementando un contatore, se si arriva a fine file, si torna il contatore. Ora definiamo una funzione che conta i caratteri di un file:
let count_fle name = try let ch = open_in name in print_string (name ^ : ); print_int (count_channel ch 0); print_string \n; close_in ch with Sys_error f -> print_endline f;;
In pratica se non ci sono problemi di apertura di file, stampa: Nome file: n
Programmazione funzionale
57
ARRAY
Gli array sono vettori di dimensione finita ed omogenei (spiegazione di omogeneit nel capitolo delle liste). La sintassi dellarray
Quindi la sintassi dellarray richiede un | dopo o prima delle quadre. Rispetto le liste, possiamo raggiungere lennesimo elemento come in tutti gli altri linguaggi di programmazione:
vettore.(0) ;;
-: int = 3
vettore.(15);;
Exception: Invalid_argument index out of bond
Ci sono come sempre le funzioni predefinite, riportiamo la solita tabella: Array.make Array.init Int -> a -> a array Int -> (int -> a) -> a array Crea un array di n elementi tutti uguali al secondo parametro Prende il primo parametro come parametro della funzione ed inizializza un array. Ad esempio: Array.init 5 (fun x -> x);; -: int array = [|0;1;2;3;4|] Ritorna la lunghezza dellarray Ritorna lennesimo elemento dellarray Mette alla fine del primo array tutto il secondo Come List.map solo che su array Modifica lennesimo elemento di un vettore Ritorna un array identico a quello dato Come list.sort solo che su array Ritorna un array che ha gli elementi dellarray passato compreso fra gli altri due parametri passati Converte larray in lista Converte una lista in array
a array -> int a array -> int -> a a array -> a array -> a array (a -> b) -> a array -> b array a array -> int -> a -> unit a array -> a array (a -> a -> int) -> a array -> unit a array -> int -> int -> a array
Esistono altri milioni e milioni di funzioncine come iter, fold_left ecc che non credo sia necessario riportare su questa guida, comunque sulle slide ci sono.
58
Programmazione funzionale
RECORD
I record sono una nominata (che ha nome) collezione di tipi arbitrari. Funziona come le struct in c:
Ogni membro deve avere un unico nome. Sintassi generica: type name = { [name : type] }
Poi si possono fare tutte le operazioni su questo tipo di dati come si facevano in c.
MEMORIZZAZIONE
Lultimo capitol lasciato al lettore poich ritenuto arcane, oscuro e ripieno di amenit.
Buona fortuna per gli esami, ricordate che se venite segati.. c di peggio nella vita!!!
Pace e bene!!!!