CENNI MINIMI DI PROGRAMMAZIONE FUNZIONALE IN PYTHON

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!