La principale modalità di calcolo è l`applicazione di funzioni Nei
Transcript
La principale modalità di calcolo è l`applicazione di funzioni Nei
1 La principale modalità di calcolo è l’applicazione di funzioni Nei linguaggi funzionali “puri” non esistono strutture di controllo predefinite per la realizzazione di cicli quali for, while, repeat Un programma è un insieme di dichiarazioni Tecnica algoritmica fondamentale: riduzione a sottoproblemi 2 Esempio: somma di ore Un’ora del giorno si può rappresentare mediante una coppia di interi (ore,minuti): int * int dove ore è compreso tra 0 e 23, e minuti è compreso tra 0 e 59. Problema: calcolare il risultato della somma di due ore Input: due coppie di interi, h1 e h2, rappresentanti ore Output: una coppia di interi, rappresentante la somma di h1 e h2 somma di (3,15) e (4,20) = (7,35) somma di (3,45) e (4,20) = (8,5) somma di (23,45) e (0,20) = (0,5) 3 Procedimento: somma_ore (h1,m1) (h2,m2): eseguire la somma dei minuti m1 e m2, ottenendo (h3,m3); sommare h1+h2+h3; sia h4 il risultato di tale somma; se h e’ il resto della divisione di h4 per 24, il risultato e’ (h,m3) 4 Sottoproblema: eseguire la somma di minuti somma_minuti di 45 e 20 = (1,5) Come otteniamo i minuti? 45 + 20 = 65 5 e‘ il resto della divisione 65/60 cioe‘ 5 = (45+20) mod 60 Come otteniamo le ore? 1 è il risultato della divisione intera (45+20)/60 Procedimento: somma_minuti (m1,m2): sia m=m1+m2; il risultato e’ (m/60, m mod 60) In OCaml: (* somma_minuti : int * int -> int * int *) let somma_minuti (m1,m2) = let m = m1+m2 (* sia m=m1+m2 *) in (m/60, m mod 60) (* nell’espressione (m/60, m mod 60) *) 5 Il programma principale Procedimento: somma_ore (h1,m1) (h2,m2): sia (h3,m3) = somma_minuti (m1,m2); il risultato e’ ((h1+h2+h3) mod 24,m3) La funzione principale: (* somma_ore : (int * int) * (int * int) -> int * int *) let somma_ore ((h1,m1),(h2,m2)) = let (h3,m3) = somma_minuti (m1,m2) in ((h1+h2+h3) mod 24, m3) 6 Esercizio: scrivere un programma che, data la rappresentazione di una data nella forma (giorno,mese), calcoli la data che rappresenta il giorno successivo. Ad esempio si avrà: giorno_succ (24,marzo) = (25,marzo) giorno_succ (28,febbraio) = (1,marzo) giorno_succ (31,dicembre) = (1,gennaio) 7 Nei linguaggi funzionali “puri” non esistono strutture di controllo predefinite per la realizzazione di cicli quali for, while, repeat, ma il principale meccanismo di controllo è la ricorsione. RICORSIONE La ricorsione è una tecnica per risolvere problemi complessi riducendoli a problemi più semplici dello stesso tipo. Per risolvere un problema ricorsivamente occorre: 1. Identificare i casi semplicissimi (casi di base) che possono essere risolti immediatamente 2. Dato un generico problema complesso, identificare i problemi più semplici dello stesso tipo la cui soluzione può aiutare a risolvere il problema complesso 3. Assumendo di saper risolvere i problemi più semplici (ipotesi di lavoro), determinare come operare sulla soluzione dei problemi più semplici per ottenere la soluzione del problema complesso 8 Esempio: la funzione fattoriale IN → IN n! = 1 × 2 × ... × (n − 1) × n Per convenzione: 0! = 1 f act(n): metodo ricorsivo per calcolare n! 1. Caso base: f act(0) = 1 2. Volendo calcolare f act(n), per n > 0, calcolare f act(n − 1) 3. Per ottenere f act(n), moltiplicare f act(n − 1) per n Definizione ricorsiva del fattoriale: f act(n) = 1 se n = 0 n × f act(n − 1) altrimenti Il fattoriale è “definito in termini di se stesso”, ma per un caso “più facile”. 9 In OCaml let rec fact n = if n=0 then 1 else n * fact(n-1) La parola chiave rec è necessaria per indicare che la definizione è ricorsiva # let fact n = if n=0 then 1 else n * fact(n-1);; Characters 36-40: let fact n = if n=0 then 1 else n * fact(n-1);; ^^^^ Unbound value fact 10 Calcolo mediante riduzioni Processo di calcolo del valore di fact 3: fact 3 = = = = = = = = = = = = if 3=0 then 1 else 3 * fact(3-1) 3 * fact(3-1) 3 * fact 2 3 * if 2=0 then 1 else 2 * fact(2-1) 3 * (2 * fact 1) 3 * (2 * if 1=0 then 1 else 1 * fact(1-1)) 3 * (2 * (1 * fact 0)) 3 * (2 * (1 * if 0=0 then 1 else 0 * fact(0-1))) 3 * (2 * (1 * 1)) 3 * (2 * 1) 3 * 2 6 Che succede se fact à applicata a un numero negativo? 11 Definizioni ricorsive di funzioni per calcolare F (n): se n e’ un caso base, riporta la soluzione per il caso n altrimenti: risolvi i problemi piu’ semplici F (n1), F (n2), ....F (nk ) (* chiamate ricorsive *) combina le soluzioni ottenute e riporta combina(F (n1), F (n2), ....F (nk )) Un processo ricorsivo termina se le chiamate ricorsive si avvicinano ai casi di base: dopo un numero finito di chiamate ricorsive si arriva a casi base. 12 Esempio: calcolo del massimo di una serie di numeri letti da tastiera Esempio: dato un numero intero non negativo n, calcolare il massimo di una sequenza di n numeri interi letti da tastiera. Operazioni di lettura e scrittura: programmazione imperativa Input da tastiera read_line : unit -> string read_int : unit -> int read_float : unit -> float # let x=read_int ();; 45 val x : int = 45 # let x=read_line();; pippo e pluto val x : string = "pippo e pluto" 13 # let x=read_int();; 3pippo Exception: Failure "int_of_string". 14 Un algoritmo ricorsivo per risolvere il problema Risolvere il problema di determinare il massimo di n−1 numeri è più semplice di quello di determinare il massimo di n numeri: possiamo risolvere il problema ricorsivamente. Procedimento: max_n (n): se n<=0 riportare 0; altrimenti leggere il primo intero x1; se n=1 riportare x1 stesso, altrimenti riportare max x1 (max_n (n-1)) Il programma (* max_n : int -> int *) let rec max_n n= if n<=0 then 0 else let x = read_int() in if n=1 then x else max x (max_n (n-1)) 15 Esempio 2 Leggere da tastiera una sequenza di interi positivi e calcolarne il massimo. La sequenza termina con un numero non positivo. Lettura di un valore che deve essere utilizzato più di una volta: Leggere un intero x e riportare la coppia (100/x, 200/x) (100/read_int(), 200/read_int()) ⇐ NO! # let x = read_int() in (100/x, 200/x);; 25 - : int * int = (4, 8) # let x = read_int() in let y = read_int() in (x/y, x mod y);; 10 3 - : int * int = (3, 1) 16 Algoritmo ricorsivo max_of : leggi un numero x; if x <= 0 then return x else return il massimo tra x e il massimo tra i numeri letti successivamente (* nuova chiamata a max_of *) (* max_of: unit -> int *) let rec max_of () = let x = read_int() in if x <= 0 then x else max x (max_of ()) 17 Esempio 3 Problema: Definire una funzione che, dato un intero n, legga da tastiera una sequenza di interi, separati tra loro da Enter, e terminata dall’immissione della stringa vuota, e riporti true se tutti i numeri letti sono minori di n, false altrimenti. Utilizziamo read_line per la lettura di ciascuna linea; dopo aver verificato che la stringa letta è diversa dalla stringa vuota "", la trasformarmiamo in un intero con la funzione di conversione int_of_string: string -> int Prima versione let rec tutti_minori_di n = let stringa = read_line() in if stringa="" then true else let k = int_of_string stringa in k<n && tutti_minori_di n In questo caso, la chiamata ricorsiva non “diminuisce” alcuna misura. È ovvio che se non si immette mai la stringa vuota il programma non termina! 18 Esempi di esecuzione # tutti_minori_di 100;; 5 6 8 10 - : bool = true # tutti_minori_di 100;; 7 200 - : bool = false Il programma non ha aspettato l’immissione della stringa vuota!! La valutazione delle espressioni booleane è “pigra”: quando si valuta k<n && tutti_minori_di n: viene valutato k<n: se è falso, questo è sufficiente a determinare che tutta la “congiunzione” è falsa, quindi non viene effettuata la chiamata ricorsiva. 19 Seconda versione Usando dichiarazioni locali si può forzare l’ordine di valutazione delle espressioni: let rec tutti_minori_di n = let stringa = read_line() in if stringa="" then true else let k = int_of_string stringa in let result= tutti_minori_di n in k<n && result