Sei sulla pagina 1di 58

Programmazione funzionale

PROGRAMMAZIONE
FUNZIONALE

Appunti e spiegazioni by Gianluca Bonacin & Federico Moschen

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

+. * *. / /. ** <, >, <=, >=, =, <> (diverso) Mod ^ && || not

ALBERO DI SINTASSI ASTRATTA


Ogni funzione ha il suo albero di sintassi astratta, esso descrive il modo in cui il computer ragiona quando si trova a che fare un una funzione. Come gia detto, le funzioni devono restituire un valore, quindi possiamo trattare le funzioni traquillamente come parametri. In pratica, se noi dovessimo passare il valore 5 per qualche motivo, potremo tranquillamente passare 3+2, poich la funzione + restituisce un intero, che in questo caso 5. Ogni nodo dellalbero ha un tipo, pu corrispondere ad un operatore, variabile o ad una costante. Ad esempio, lalbero di sintassi astratta di 3 + 5 :

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:

Programmazione funzionale Int +

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

Bool ma + ritorna int

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:

let NomeNunzione parametro1 parametro2 .. paramentroN = CorpoFunzione ;;


Let un operatore che concettualmente significa sia come in matematica. Ad esempio avendo la funzione:

let add1 x = x+1;;

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:

val add1: int -> int = <fun>


Questo vuole dire che la funzione add1, ha come parametro un intero (il primo int prima della freccia) e ritorna un intero (lint dopo la freccia), ed una funzione ( = <fun>). Come gia detto in precedenza, le funzioni possono essere passate come paramentro, limportante che soddisfino la tipizzazione richiesta:

add1 (add1 1);;


Qui si passa alla funzione pi esterna il risultato di quella pi interna. Lalbero di sintassi astratta il seguente: int add1

int

add1

int

<questo + la seconda chiamata>

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:

let add2 x = (add1 x) + 1;;

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.

add1 7;; Passaggio interno

{add1-> <fun>, x ->2} {x->7, add1-> <fun>, x ->2}

Risultato: 8

{add1-> <fun>, x ->2}

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 7;; Passaggio interno Risultato: 33

{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

let media x y = (x +. y) /. 2.0;;


float /.

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:

val media: float -> float -> float = <fun>


Lultimo tipo prima di = <fun> il tipo del risultato della funzione. Lo si individua guardando il tipo del nodo radice(nodo a top level) dellalbero di sintassi della nostra funzione.

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:

print_string print_char print_int print_float

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:

print_string Hello world;; Risultato: Hello world- : unit = ()


Si possono gestire i caratteri speciali come /n ecc come nel c++. NB: possiamo computare una sequenza di istruzioni separandole luna dallaltra con il singolo punto e virgola, lultima sar seguita dal doppio punto e virgola.

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:

if expression1 then expression2 else expression3


Se verificata lespressione1, allora fai lespressione2, altrimenti fai lespressione3. Evidentemente, lespressione1 deve restituire un booleano, quindi nella maggior parte dei casi, si tratta di una comparazione. Lespressione2 e lespressione3 devono restituire il medesimo tipo. Ad esempio:

if 1<2 then 2.0 else 3.0


In questo caso, viene ritornato 2.0 perch il controllo ha avuto un esito positivo. if 2.0 3.0 < bool float float

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.

let print_bool x = if x then print_string true else print_string false;;


In questo caso x viene tipizzata a booleano, perch il tipo di parametro che richiede if, se x vera viene richiamata la print_string con parametro true, resitituendo quindi unit, mentre se falsa viene richiamata la print_string con paramentro false, restituendo ancora unit. Lalbero di sintassi astratta (dimostrativo ricordo) : unit print_string if print_string unit

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

-. : float * float -> float

score -. 1.0

Sottrazione

ceil : float -> float floor : float -> float

ceil 9.5 floor 9.5

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

Mentre per gli interi abbiamo:


- : int -> int * : int * int -> int / : int * int -> int -5, -limit 2 * limit 7 / 3, score / average Negazione unaria Moltiplicazione Divisione intera Resto divisione, il risultato intero ed ha il segno del primo operando Addizione Sottrazione Valore assoluto

mod : int * int -> int limit mod 2

+ : int * int -> int - : int * int -> int abs : int -> int

2 + 2, limit + 1 2 - 2, limit - 1 abs (-5)

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.

let polar_of_cartesian (x,y) = ( sqrt(x ** 2.0 +. y**2.0), atan(y /.x));;


In questo caso la funzione richiede (x,y) come parametro: qualcosa racchiuso fra parentesi e separato da virgole sempre una tupla. Al momento non sappiamo ancora fra che insiemi stiamo facendo il prodotto cartesiano, ma guardando il corpo della funzione, ci accorgiamo che una tupla di float. Nelle tuple, sono fondamentali le parentesi, notate come vengono usate per costruire la tupla di ritorno: una prima parentesi che sta a significare che si sta costruendo una tupla, la seconda per passare i parametri a sqrt, una terza per passare i parametri ad atan (arcotangente) e la successiva chiusura, per dire che la tupla finita. Notate la virgola fra le due funzioni, quella virgola la stessa di (1, 2): serve a separare gli elementi della tupla, che in questo caso sono due funzioni.

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):

let rec factorial n = if n = 0 then 1 else n*(factorial(n-1));;


Con scheletro: val factorial : int -> int = <fun>. Che avr come albero di sintassi astratta:

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:

match n with 0 -> 0 | x -> x + 1;;


In questo caso confronta n con il pattern 0 e con il pattern x. In pratica se n 0 allora ritorna 0, altrimenti se n ha un valore (se n x) ritorna il suo valore pi uno. Spesso non dobbiamo legare il pattern con lespressione che stiamo esaminando. Per fare un caso generico (spiegato meglio nellesempio in seguito) usiamo il simbolo _:

match n with 0 -> 0 | _ -> n + 1;;

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:

a -> a list -> a list

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];;


Ci restituir una lista composta da:

[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.

PATTERN MATCHING APPLICATO ALLE LISTE


Tramite il pattern matching possiamo eseguire molte operazioni utili sulle liste. Possiamo confrontare una list con vari pattern, ad esempio:

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:

let rec positive_list ls = match ls with [] -> [] | x::xs -> if x > 0

then x :: positive_list xs else positive_list xs;;

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 rec sum n = if n = 0 then 0 else n + sum (n-1);;


Facile vedere cosa faccia questa funzione, difficile accorgersi che NON tail recursion? Perch? Perch lultima operazione non una chiamata pura bens un operazione di somma su una chiamata ricorsiva. Questa funzione tail recursion fatta in questo modo:

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

PUNTO DELLA SITUAZIONE


A questo punto possiamo fare alcune osservazioni riguardo a quanto fatto fin ora. Per prima cosa, nel pattern matching non obbligatorio utilizzare sempre il nome x::xs, possiamo usare qualsiasi nome di variabile, come y,zorro,k. Noi per convenzione useremo una singola lettera per il primo elemento della lista e la stessa lettera con una s aggiunta per il resto della lista, ad esempio y::ys. Ulteriore precisazione:

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

Questa la nostra funzione id, ma lo anche questa: id : float x : float

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 swap (x,y) = (y,x);;


facile capire che questa una funzione che data una tupla, ne restituisce un altra con gli elementi invertiti. Ma su che tipo di tupla lavora? Su tuple di interi? Potrebbe essere, ma non solo, quindi diremo che lavora su tutti i tipi di tuple, quindi su una tupla di (a, b) e restituisce una tupla di (b,a). Possiamo dire che gli operatori di confronto sono polimorfi, perch funzionano con tutti i tipi di parametri. Abbiamo gia visto in precedenza un piccolo esempio sulla lunghezza di una lista, bene, quello era un esempio di polimorfismo, andatelo a rivedere se non ve lo ricordate. Sulle slide sono presenti numerosi esempi di polimorfismo, tutti gi visti sul capitolo delle liste e della ricorsione, se avete dubbi rivedeteli. Esaminiamo ora un esempio non visto in precedenza:

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:

pattern when condizione


Il when ha il significato letterale, in pratica accedi al pattern se esso vero quando la condizione verificata. La condizione deve restituire un booleano, come fare un if, dentro una voce dello switch (dove lo switch il pattern matching). La funzione riportata come esempio dato un qualsiasi x ed una lista xs di tipi del tipo di x confronta xs con la lista vuota, se vuota ritorna falso, se ha degli elementi, confronta il primo col parametro x se sono uguali ritorna vero e stoppa la ricorsione, altrimenti fa la ricorsione su x ed il resto della lista.

20

Programmazione funzionale

PATTERN MATCHING ESAUSTIVO


Il pattern matching deve essere esaustivo: deve esaminare tutti i casi. In pratica per ogni valore dellespressione che sia analizza, deve esserci almeno un pattern associato. Ad esempio questo non esaustivo:

let foo x = match x with 0 -> 0 |1 -> 2 |2-> 3;;


In questo caso, vengono analizzati solo i casi in cui x sia 0,1,2 e non tutti gli altri infiniti casi, se gli si passa 5, cosa succede? Boh chiedetelo al Cimatti. Un esempio di pattern esaustivo:

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.

CITTADINI DI PRIMA CLASSE (-_-)


Come gia detto le funzioni devono restituire un valore, quindi possiamo metterle al posto di qualsiasi tipo di codice, al posto di una variabile, oppure al posto di un altra funzione. Noi abbiamo esaminato due funzioncine molto semplici sulle liste, la add1 e la positive, ricordiamole:

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

Prendiamo come esempio la funzione map, verr spiegato in seguito:

let rec map funzione lista = match lista with [] -> [] | x::xs -> funzione x :: map funzione lista;;
I tipi di questa funziuone saranno:

val map : (a -> b) -> a list -> b list = <fun>


Map perende come parametro una qualsiasi funzione, che restituisce un qualsiasi tipo, e richiede un solo parametro, ed una lista qualsiasi. I tipi della funzione sono quelli fra parentesi. La funzione viene tipizzata a (a -> b) perch al momento della sua chiamata,

| x::xs -> funzione x ::


le viene passato x che il primo elemento della lista (perch si fatto il match su lista che il secondo parametro di map), la lista di tipo a list, e quindi x che il suo primo elemento sar un a. Restituisce un b, perch a priori non sappiamo che lavoro faccia. La funzione principale, map restituisce un b list perch fa il cons

funzione x :: map funzione lista;;


sulla funzione che richiede come parametro (la funzione come detto 2 righe sopra restituisce un b). Quindi possiamo dire che map fa qualcosa in base alla funzione che gli si passa come parametro, limportante che gli si passi una funzione del tipo (a -> b). Per costruire una map che funzioni con una funzione del tipo (a -> a -> b) bisogna modificarne il corpo, ed in particolare il momento della chiamata. Ecco un esempio di uso della map:

22

Programmazione funzionale

let add1 ls = let add x = x+1 in map add ls;;


Add1 ha come parametro una lista, si dichiara una funzione locale di tipo (int > int) che si chiama add, e si richiama la map passandogli come parametro add e ls. In pratica la map ora svolge questo lavoro: match ls with [] -> [] | x::xs -> add x :: map add ls;;

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:

let positive ls = let pos x = x > 0 in map pos ls;;


Come sopra, solo che questa volta la funzione applicata pos che fa un lavoro diverso da add e restuisce anche un parametro diverso da add. Map quindi funzioner in questo modo: match ls with [] -> [] | x::xs -> pos x :: map pos ls;;

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.

Programmazione funzionale Esempio interessante quello di fold_left:

23

let rec fold_left f x xs = match xs with [] -> x | y::ys -> fold_left f (f x y) ys;;

Vista cos abbastanza

CRIPTICA. Ma noi stoicamente ed eroicamente la spiegheremo: allora, ha tre

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:

let sum ls = fold_left (+) 0 ls;;


Ora spieghiamo passo per passo: supponiamo di fare sum [5;2;8]. Il rimo passo questo: sum [5;2;8];;

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) [];;

24 Questo sar lultimo passo: match [] with [] -> x | indifferente

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 all ls = fold_left (&&) true ls;;


Questa in pratica fa land fra tutti gli elementi di una lista booleana, restituisce vero se e solo se tutti gli elementi della lista sono veri, se ce n uno falso ritorna falso (rivedersi le propriet della and se non si capisce il perch). Si pu passare alla fold left anche la concatenazione:

let concat ls = fold_left (@) [] ls;;


Un altro tipo di funzione generalizzata molto importante il filtro, vediamo come e fatto e cerchiamo di capirlo:

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 var -> espressione


ad esempio:

fun x -> x + 1
Per utilizzarla scriveremo ad esempio:

(fun x -> x + 1) 7;;

APPLICAZIONE PARZIALE DELLE FUNZIONI


Se valutiamo una funzione, supponendo che non abbia parametri, il valore di ritorno comunque una funzione. Ad esempio:

let add1 x = x + 1;;


Possiamo scriverla come:

let add1 = (+) 1;;


Quando si andr ad usare facendo ad esempio add1 5;;

Andr a sostituire il suo contenuto al posto di add1: (+) 1 5

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:

let positive = filter ((<) 0);;


Sinceramente un po ambiguo, per sempre meglio sapere che esiste una scrittura del genere per lesame, non ne capiamo lutilit. Filter labbiamo vista sopra, se non vi ricordate riguardatela.

26

Programmazione funzionale

FUNZIONI COME PARAMETRI


Come gia detto un mucchio di volte in precedenza, le fuzioni possono essere usate come parametri, ad esempio:

let apply f x y = f x y;;


Che ha come tipi:

apply : (a -> b -> c) -> a -> b -> c = <fun>

Primo parametro: una funzione

secondo parametro

terzo parametro

ritorno

Che ad esempio potrebbe essere utilizzata in questo modo:

let add x y = apply (+) x y;;


Queste funzioni sono chiamate funzioni di ordine superiore.

FUNZIONI PREDEFINITE DELLE LISTE


Funzione
:: : 'a -> 'a list -> 'a list @ : 'a list -> 'a list -> 'a list List.length : 'a list -> int List.hd : 'a list -> 'a List.tl : 'a list -> 'a list List.nth : 'a list -> int -> 'a List.rev : 'a list -> 'a list

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:

type seme = Cuori | Picche | Quadri | Fiori;;


Questa dichiarazione crea un nuovo tipo chiamato seme con quattro distinti valori che pu assumere. Questo nuovo tipo pu essere usato come i tipi predefiniti. I nuovi valori sono costanti come falso, vero, 0, 1 ecc. Ad esempio potremo creare una funzione che data una variabile di tipo seme, la converte in stringa:

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:

type carta = Carta of seme * int;;


Il simbolo Carta (c maiuscola) chiamato costruttore. simile ad una funzione che prende un paio di tipi (seme * int) e crea un valore di tipo carta.Come le costanti i costruttori possono essere usati nel pattern matching. Ad esempio:

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 =

Nome1 [of type] | Nome2 [of type] | NomeN [of type];;

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

Per definizione di tipi, un albero binario di a descritto come:

type a tree =

Empty | Tr of a * a tree * 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));;

30 Si rappresenter graficamente come: 1

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)

Avr come risultato: 1

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.

let root t = match t with Tr(r, _, _) -> r | _ -> raise EmptyTree;;


val root : a tree -> a = <fun>

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.

let left t = match t with Tr(_, l, _) -> l | _ -> raise EmptyTree;;


val left : a tree -> a tree = <fun>

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));;

Graficamente rappresentato in questo modo: 1

4 E facessimo : Left albero;;

Essa fornirebbe come risultato: - : int tree = Tr (2, Tr (4, Empty, Empty), Tr (5, Empty, Empty))

32 Che si rappresenta in questo modo: 2

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.

let right t = match t with Tr(_, _, r) -> r | _ -> raise EmptyTree;;


val right : a tree -> a tree = <fun>

right albero;; - : int tree = Tr (3, Tr (6, Empty, Empty), Empty)

Ulteriore funzione la is_empty. Dato un albero dice se vuoto oppure no. Quindi ritorna un booleano.

let is_empty t = t = Empty;;


val is_empty : a tree -> bool = <fun>

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).

let leaf l = Tr(l, Empty, Empty);;


val leaf : 'a -> 'a tree = <fun>

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>

La funzione size cont ail numero di nodi presenti in un albero.

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>

34 Per livello si intende:

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

ALBERI BINARI BILANCIATI


Un albero binario bilanciato se per ogni nodo la differenza fra la profondit del sottoalbero sinistro e di quello destroy al massimo uno. Si pu valutare se un albero binario bilanciato tramite la funzione balanced.

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);;

Programmazione funzionale Avremo come risultato: - : int list = [1; 2; 4; 5; 3; 6; 7]

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>

Ricordando che fulltree 2 : 1

38

Programmazione funzionale

COSTRUIRE UN ALBERO BILANCIATO


Cerchiamo ora di costruire un albero bilanciato partendo da una lista. Quando la lista vuota, allora anche lalbero sar vuoto, e quindi bilanciato. Quando la lista matcha con x::xs, lalbero che ritorneremo sar bilanciato se abbiamo un nodo con un elemento x che punta a due sottoalberi bilanciati ottenuti dalla chiamata ricorsiva della prima e la seconda met di xs. NB: da questo momento in poi, mi sembra inutile spiegare ogni volta tutte le funzioni, mi sembra pesante ed inutile, come ho scritto gi molte volte, se non si capisce ci che segue, sarebbe meglio tornare indietro e rileggersi i concetti fondamentali. La prima funzione la take:

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]

Ora usiamo queste due nella balpreorder:

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)))

Albero fatto in questo modo: 1

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

VISITA DI UN ALBERO CON ACCUMULATORE


Vogliamo una funzione che attraversi un albero e ritorni una lisrta di tuple composta da ogni elemento diverso che si trova nellalbero e quante volte compare. Ad esempio avendo un albero di questo tipo:

Ci dovrebbe ritornare: [(4,2); (1,2); (3,2); (7,1)]

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

FUNZIONI DI ALTO LIVELLO DEGLI ALBERI


Come nelle liste, anche per gli alberi esistono funzioni di alto livello. Per funzioni di alto livello intendiamo quelle funzioni tipo la List.map, in cui bisognava passargli una funzione e lei la applicava a tutti gli elementi della lista. Negli alberi vedremo la treemap:

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

La struttura che ne consegue sar:

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

Expr * Expr Variable String Mul

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 ^ )

-> string_of_float c -> v -> string_of_expr e1 ^ + ^ string_of_expr e1 -> ( ^ string_of_expr e1 ^ ) * ( ^ string_of_expr

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>

Ad esempio vogliamo rappresentare lespressione (7.0 * x) + (3.0 * y)

Tramite il tipo expr. Scriviamo sul cammello:

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

let empty = [];;


Successivamente dichiariamo la funzione assign. Essa servir ad inserire nella nostra lista ambiente una variabile e cosa legato ad essa.

let assign m v x = (v,x)::m;;


val assign : (a * b) list -> a -> b -> (a * b) list = <fun>

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):

let m = assign empty x 1.;;


Gli si passa empty perch la prima variabile che leghiamo a qualcosa. La lista a questo punto sar: val m : (string * float) list = *(x, 1.)+

Ora vogliamo inserire un'altra variabile y e legarla a 2.

let m = assign m y 2.;;


La lista a questo punto diventa: val m : (string * float) list = *(y, 2.); (x, 1.)+

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

E facendo lookup m x;;

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.

Se poi noi riassegniamo un valore ad x facendo: let m = assign m x 3.;;

Crea una nuova lista ambiente fatta in questo modo: *(x, 3.);(y, 2.); (x, 1.)+

Quindi ricercando di nuovo x avremo: lookup m x;; :- float = 3

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.

TIPO OPTION PREDEFINITO


I tipi predefinito sono per lo pi definiti come: type a list = *+ | :: of (a * a list);;

I bool come: type bool = false | true;;

Unit definite come: Type unit = ();;

Esiste un tipo option predefinito, dichiarato nel seguente modo:

type a option = None | Some of a;;


Questo tipo pu essere usato quando I valori non sono sempre ben definiti. Immaginiamo di dichiarare una funzione che data una lista l1 restituisce None se l1 e' vuota. Se l1 non e' vuota restituisce invece Some (x, count), dove x e' l'elemento che appare per piu' volte consecutivamente all'interno di l1 mentre count e' il numero di volte per cui x si ripete. Questo molto pi pulito di restituire (0,0) nel caso la lista fosse vuota. Possiamo usare questo tipo nel lookup:

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>

48 Ora definiamo la somma usando la funzione op definita in precedenza:

Programmazione funzionale

let sum = op (+.);;


val sum : float option -> float option -> float option = <fun>

E la moltiplicazione come:

let mul = op (*.);;


val mul : float option -> float option -> float option = <fun>

Ora cerchiamo nuovamente di valutare le espressioni in questo modo:

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:

Programmazione funzionale Gli alberi ennari sono definiti in questo modo:

49

type a ntree = Empty | Tr of a * a ntree list;;


La teoria di lavorare sugli alberi ennari la stessa degli alberi binari, bisogna andare in ricorsione su tutti i figli. In seguito ci sono tutte le funzioni gia fatte sugli alberi binari, adattate agli ennari:

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

La remove invece ripristina il riferimento precedente ad un elemento, sempre se esiste:

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

E facendo Hashtbl.remove gatto avremo: Elemento gatto cane Riferimento 1 2

La funzione copy data una hash table ne restituisce la copia:

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>

Qundi avendo: Elemento gatto cane gatto Riferimento 1 2 5

E facendo Hashtbl.find t gatto avremo come risultato: -: int = 5

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>

52 Quindi avendo: Elemento gatto cane gatto Riferimento 1 2 5

Programmazione funzionale

Avremo come risultato: -: int list = [1;5]

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

Ne esistono altri, ma non li scriviamo. I moduli possono essere aperti scrivendo:

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:

let hw () = print_string Hello, World\n;; hw ();;


Per la compilazione fare: ocamlc hw.ml Ocamlopt hw.ml Per lesecuzuione in Windows $ ./camlprog

Per lesecuzione in unix: $ ./a.out

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

Vogliamo ora contare il numer di righe di un qualsiasi input ad esempio:


file.txt

This is a sample file

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:

let rec lc n = let _ = read_line() in lc (n+1);; lc 0;;


Quando ci fermiamo? Come riusciamo a capire che ci troviamo alla fine del file? Testando il codice scritto sopra viene sollevata un eccezione mega mega: END_OF_FILE. Vi fa pensare a qualcosa? A me si:

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

Con ritorno a capo.

Programmazione funzionale

57

ARRAY
Gli array sono vettori di dimensione finita ed omogenei (spiegazione di omogeneit nel capitolo delle liste). La sintassi dellarray

let vettore = [|3;5;7;9|];;


-: int array = [|3;5;7;9|]

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

Se si va fuori dalla dimensione del vettore viene sollevata un eccezione:

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

Array.length Array.get Array.append Array.map Array.set Array.copy Array.sort Array.sub

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

Array.to_list a array -> a list Array.from_list a list -> 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:

type complex = { real : float; img : float };;


type complex = { real : float; img : float; }

come se avessimo un tipo composto da real float img float

Ogni membro deve avere un unico nome. Sintassi generica: type name = { [name : type] }

Per le assegnazioni si fa:

let x = { real = 1.0; img = 0.0}


Poi possiamo accedere ad uno dei campi facendo x.real;; -: float = 1

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!!!!

Potrebbero piacerti anche