CENNI MINIMI DI PROGRAMMAZIONE FUNZIONALE IN PYTHON - V. 0.3 MATTEO PRADELLA 1. Introduzione e concetti base La programmazione funzionale è uno stile di programmzione che enfatizza la valutazione di espressioni, piuttosto che l’esecuzione di comandi. Le espressioni in questi linguaggi sono costruite per mezzo di applicazioni di funzioni che combinano valori di base. Un linguaggio funzionale supporta ed incoraggia la programmazione in stile funzionale. Esempio tipico di linguaggio con un approccio essenzialmente funzionale è il Lisp. Noi proveremo con Python. Per esempio, consideriamo il problema di calcolare la somma degli interi tra 1 e 10. In un linguaggio imperativo (es. il C) si può fare per mezzo di un ciclo e di una variabile che memorizza i risultati intermedi: int tot = 0; for (int i=1; i<=10; ++i) tot += i; In un linguaggio funzionale lo stesso programma viene espresso senza senza variabili e assegnamenti. Per esempio in Python in stile funzionale si può fare cosı̀: reduce(lambda a,b : a+b, range(1,11)) Occhio che Python non è di per se funzionale! Potevamo fare la stessa cosa in stile C: tot = 0 for i in range(1,11) : tot += i In breve, il paradigma funzionale è basato sui seguenti caposaldi: (1) La variabile riassume il suo significato matematico (non è più una cella di memoria); (2) i dati sono espressioni simboliche manipolate per mezzo di funzioni (funzioni pure, senza effetti collaterali); (3) i programmi sono le funzioni; (4) strumento fondamentale per la definizione delle funzioni: ricorsione; (5) assegnamento = male! Date: A.D. MMIII. 1 2 MATTEO PRADELLA Python permette di valutare una espressione simbolica per mezzo di eval. Per esempio eval("1+2") restituisce 4. 2. Le Liste: accesso e costruzione Ora introduciamo qualche funzione a-la Lisp sulle liste, che useremo come principali strutture dati. Per prima cosa definiamo atomo: servirà spesso come test per la chiusura di una ricorsione su liste. 2.1. Atomo. Un atomo è una lista vuota, oppure una non-lista. def atom (l) : return l == [] or not type(l) is list 2.2. Funzioni di accesso. Approccio stile Lisp per accesso alle liste: first restituisce il primo elemento di una lista, mentre last restituisce il resto della lista (una lista anch’essa). Notate analogia con ricorsione: first - caso base, last - passo ricorsivo. Nota: Python fornisce funzioni di accesso a liste molto potenti, e ne permette la modifica degli elementi - non è quello che vogliamo fare! def first(l) : return l[0] def rest(l) : return l[1:] Se ci serve, possiamo usare direttamente l’operatore di slice (es. a[2:5]). 2.3. Costruttore. Useremo come costruttori di liste + e [ ]. Una nota culturale: il Lisp usa come costruttore l’operatore cons, che può essere definito in Python in questo modo: def cons(e,l) : return [e]+l Cons è fondamentale in Lisp, perché le liste del Lisp sono implementate per mezzo di coppie. Per esempio, la lista [1, 2, 3] in Lisp viene implementata come (1, (2, (3, nil))) (in Python), dove nil è un valore speciale che denota la lista vuota. Inoltre in Lisp gli stessi programmi sono liste, dunque si possono manipolare in modo analogo ai dati. 3. Esempi 3.1. Esempio 1: implementazione di reverse. Vogliamo definire una funzione reverse che, data una lista, ne restituisca la versione rovesciata. Per esempio: reverse([1,2,3]) restituisce [3,2,1]. def reverse(l) : if atom(l) : return l else : return (reverse(rest(l)) + [first(l)]) CENNI MINIMI DI PROGRAMMAZIONE FUNZIONALE IN PYTHON - V. 0.3 3 3.2. Esempio 2: appiattiamo una lista. Appiattiamo la nostra lista di liste! Es. flatten([[1],[2,[3]],[4]]) restituisce [1,2,3,4]. def flatten(l) : if l == []: return l elif atom(l) : return [l] else : return flatten(first(l)) + flatten(rest(l)) 3.3. Esempio 3: calcolo profondità. Profond: restituisce la profondità di una lista. Es. profond([[1],[2,[3]],[4]]) restituisce 3. def profond(l) : if atom(l) : return 0 else : return max(profond(first(l))+1,profond(rest(l))) 3.4. Esempio 4: l’append del Lisp. La funzione append in Lisp ha lo stesso ruolo dell’operatore + su liste in Python: append([1,2],[3,4]) restituisce [1,2,3,4]. Proviamo ad implementarla usando cons, dunque in stile Lisp: def append_lisp(x,y) : if x == [] : return y else : return append_lisp(reverse(rest(reverse(x))),\ cons(first(reverse(x)),y)) 4. Da iterazione a ricorsione Un approccio funzionale puro non fa uso dell’iterazione: i cicli vengono espressi per mezzo di opportune funzioni ricorsive. In pratica gli strumenti necessari per scrivere un programma che segua il paradigma funzionale sono: il costrutto if-then-else, i confronti, liste e atomi e relative funzioni di accesso/costruttori, la definizione di funzioni (ricorsive). Come possiamo ottenere un analogo funzionale di codice iterativo? Facciamo un esempio di ciclo doppio: def pcart(A,B) : r = [] for a in A : for b in B : r += [[a,b]] return r In pratica abbiamo definito un prodotto cartesiano tra liste, per esempio: 4 MATTEO PRADELLA >>> pcart([1,2,3],[’a’,’b’]) [[1, ’a’], [1, ’b’], [2, ’a’], [2, ’b’], [3, ’a’], [3, ’b’]] Come fare lo stesso doppio ciclo adottando un approccio funzionale puro? Be’, per esempio in questo modo: def ciclo_b(a,B) : if B == [] : return [] else : return [[a,first(B)]] + ciclo_b(a,rest(B)) def pcartf(A,B) : if A == [] : return [] else : return ciclo_b(first(A),B) + pcartf(rest(A),B) In pratica la funzione ciclo b ha il ruolo del ciclo for sulla lista B, mentre il ciclo esterno viene eseguito direttamente da pcartf . 5. Viceversa: da ricorsione a iterazione Una funzione ricorsiva è detta impropria (o tail-recursive) se restituisce immediatamente il risultato della sua chiamata ricorsiva, cioè non la usa come argomento di un’altra funzione. Per esempio, si può costruire una versione ricorsiva impropria della reverse vista prima. Possiamo sfruttare un nuovo parametro per memorizzare i risultati intermedi del calcolo (lo chiamiamo r): def rev_tr(l, r=[]): if atom(l) : return r else : return rev_tr(rest(l),[first(l)]+r) In questo modo possiamo applicare la funzione più esterna (in questo caso +) rirettamente a r. Nota come il risultato di rev_tr(rest(l),[first(l)]+r) sia passato direttamente a return. Inoltre attenzione: r = [] in questo caso non è un assegnamento, ma la definizione di un valore di default per il parametro r. Dunque la struttura tipica di una funzione ricorsiva impropria è: def f(l, r=e) : if F(l) : return r else : return f(G(l),H(r)) CENNI MINIMI DI PROGRAMMAZIONE FUNZIONALE IN PYTHON - V. 0.3 5 Una funzione ricorsiva impropria è facilmente (e automaticamente) traducibile in una funzione iterativa. In effetti, molti compilatori per il linguaggio Lisp traducono automaticamente le ricorsioni improprie in iterazioni (che sono in genere più efficienti). Prendiamo come esempio la funzione f di prima - può essere tradotta in una versione iterativa in questo modo: def f_it(l) : temp = l r = e while not(F(l)) : temp0 = temp r0 = r temp = G(temp0) r = H(r0) return r Per fare un esempio reale, prendiamo rev tr: in quel caso, e = [] F (l) = atom(l), G(l) = rest(l), H(r) = [f irst(l)] + r. Dunque otteniamo: def rev_it(l) : temp = l r = [] while not(atom(temp)) : temp0 = temp r0 = r temp = rest(temp0) r = [first(temp0)] + r0 return r Nota: chiaramente questo codice non è più funzionale!