La programmazione funzionale

In questo capitolo daremo uno sguardo al modo in cui Python supporta un'altro stile di programmazione: la Programmazione Funzionale (PF). Come nel caso della ricorsione si tratta di un argomento avanzato che per il momento potete anche trascurare. Le tecniche funzionali hanno realmente alcune applicazioni nella pratica quotidiana della programmazione e gli appassionati della PF ritengono che si tratti di un metodo decisamente migliore per lo sviluppo del software.

Che cos'è la programmazione funzionale

La programmazione funzionale non deve essere confusa con la programmazione prescrittiva (o procedurale). E non è nemmeno simile alla programmazione ad oggetti. È qualcosa di differente, anche se le differenze non sono radicali, in quanto i concetti rimangono quelli ormai familiari della programmazione, sia pure espressi in modo differente. Anche la filosofia relativa al modo in cui questi concetti vengono applicati nella soluzione dei problemi è un po' differente.

La programmazione funzionale tratta espressioni. Tanto che un modo alternativo di definirla potrebbe essere programmazione orientata alle espressioni dato che nella PF tutto si riduce ad una espressione. Come ricorderete una espressione è un insieme di operandi e variabili il cui risultato è un singolo valore. Ad esempio x == 5 è un'espressione booleana;  5 + (7-Y) è un'espressione aritmetica; e string.search("Ciao gente", "Cia") è un'espressione stringa. Quest'ultima è anche una chiamata di una funzione del modulo string e, come vedremo, le funzioni sono assai importanti in PF (questo avreste anche potuto dedurlo dal nome!).

Nella PF le funzioni vengono usate come oggetti, ovvero vengono trattate in un programma in modo strettamente analogo alle variabili. Abbiamo visto esempi di questo nei programmi di tipo GUI in cui abbiamo assegnato all'attributo command di un controllo di tipo "Bottone" il nome di una funzione. Abbiamo cioè trattato la funzione di gestione dell'evento come un oggetto assegnando al Bottone un riferimento alla funzione. Questa forma di utilizzo delle funzioni in un programma è alla base della PF.

I programmi funzionali tendono anche ad essere molto orientati alle liste.

Infine la PF tende a focalizzarsi sul "che cosa" piuttosto che sul "come" nella soluzione dei problemi. Cioè un programma funzionale dovrebbe descrivere il problema da risolvere piuttosto che indicare il meccanismo di soluzione. Esistono molti linguaggi di programmazione che tendono a funzionare in questo modo; uno dei più diffusi è Haskell ed il relativo sito web (www.haskell.org) contiene numerosi articoli che descrivono la filosofia della PF oltre al linguaggio Haskell. La mia personale opinione è che il risultato, per quanto lodevole, è piuttosto sopravvalutato dai sostenitori della PF.

Un programma puramente funzionale viene strutturato definendo un'espressione che racchiude l'obiettivo del programma. Ogni termine dell'espressione è a sua volta un'affermazione relativa ad una caratteristica del problema (magari incapsulata dentro un'altra espressione) e la valutazione di tutte queste espressioni alla fine porta al risultato.

Questo per quanto riguarda la teoria. Ma funziona? Ebbene, qualche volta funziona benissimo. Per alcuni tipi di problemi si tratta di una tecnica potente e naturale. Purtroppo per altre classi di problemi richiede uno stile di pensiero decisamente astratto, fortemente ispirato ai principi della matematica. Il codice che ne risulta è spesso assai poco leggibile per il programmatore normale. Il codice risultante è anche assai spesso più conciso del corrispondente codice procedurale ed anche più affidabile. Sono queste ultime qualità che hanno indotto molti programmatori che usano tecniche procedurali o ad oggetti di tipo convenzionale ad esplorare la PF. Anche se non viene adottata totalmente la PF fornisce parecchi strumenti potenti che possono essere usati da tutti.

L'approccio di Python

Python fornisce parecchie funzioni che consentono un approccio funzionale alla programmazione. Tali funzioni sono tutte di convenienza, nel senso che potrebbero essere scritte direttamente in Python in modo assai facile. Ma quello che importa è lo "scopo" implicito nel fatto di averle definite e cioè quello di consentire al programmatore Python, se lo desidera, di lavorare in modo funzionale.

Vediamo ora alcune delle funzioni definite e come esse operano su semplici strutture che definiamo:

prosciutto = ['maiale','spalla','spezie']
numeri = [1,2,3,4,5]

def uova(arg): 
    return arg

map(laFunzione, laSequenza)

Questa funzione applica una funzione Python, laFunzione, a ciascun membro della lista laSequenza. L'espressione:

L = map(uova, prosciutto)
print L
Crea una nuova lista (che in questo caso è uguale alla lista prosciutto) nella variabile L.

Avremmo potuto ottenere lo stesso risultato scrivendo:

for i in prosciutto:
   L.append(i)
print L

Però notate che la funzione map ci consente di eliminare la necessità di un blocco di codice indentato. Da un certo punto di vista la complessità del programma risulta ridotta di un livello. Vedremo che un un tema ricorrente nella PF coinsiste appunto nella riduzione della complessità del codice per mezzo della eliminazione di blocchi.

filter(laFunzione, laSequenza)

Come suggerisce il nome la funzione filter estrae dalla sequenza tutti gli elementi per i quali la funzione argomento riporta il valore "vero". Supponiamo di voler creare una nuova lista contenente solo numeri dispari:

def dispari(n): return (n%2 != 0) # si usa l'operatore mod
L = filter(dispari, numeri)
print L

In alternativa potremmo scrivere:

def dispari(n): return (n%2 != 0)
for i in numeri:
   if dispari(i):
      l.append(i)
print L

Notate che nuovamente il codice tradizionale richiede due livelli di indentazione per arrivare allo stesso risultato. Anche qui un maggior ricorso all'indentazione è indice di maggiore complessità del codice.

reduce(laFunzione, laSequenza)

Il funzionamento della funzione reduce è un po' meno ovvio. Essa riduce una lista ad un solo valore combinando gli elementi per mezzo di una funzione passata come argomento. Ad esempio possiamo sommare gli elementi di una lista per ottenere il totale nel modo seguente:

def somma(i,j): return i+j
print reduce(somma, numeri)

Anche in questo caso potremmo ottenere lo stesso risultato in modo più tradizionale:

L = [] # una lista vuota
ris = 0
for i in range(len(numeri)): # si usano gli indici
    ris = ris + numeri[i]
print ris

Anche se il risultato prodotto in questo caso è evidentemente lo stesso, altri casi possono non essere altrettanto semplici da comprendere . L'operazione realmente effettuata da reduce consiste nel chiamare la funzione argomento passandole i primi due membri della lista e rimpiazzando poi il secondo argomento con il risultato. In altre parole una migliore rappresentazione di reduce è la seguente:

L = numeri[:] # fai una copia della lista originale
while len(L) >= 2:
   i,j = L[0],L[1] # usa l'assegnazione ad una tupla
   L = [i+j] + L[2:]
print L[0]

Ancora una volta vediamo che le tecniche PF riducono la complessità del codice eliminando la necessità di un blocco indentato.

lambda

Noterete che negli esempi mostrati le funzioni passate come argomento alle funzioni PF tendono ad essere assai brevi: spesso sono costituite da una sola linea di codice. Allo scopo di limitare lo sforzo di definire molte brevi funzioni, Python fornisce alla PF un altro strumento detto lambda.

Il termine lambda viene usato in riferimento a funzioni anonime, ovvero blocchi di codice che possono essere eseguiti esattamente come funzioni, ma privi di nome. I costrutti lambda possono essere definiti in un programma Python ovunque può comparire un'espressione, quindi anche all'interno delle funzioni PF.

Il costrutto Lambda si presenta come segue:

lambda <ListaArgomenti> : <blocco di codice che usa gli argomenti>

Quindi potremmo riscrivere la funzione somma che abbiamo visto sopra nel modo seguente:

ris = lambda i,j: i+j
E possiamo evitare del tutto la linea di definizione creando la lambda all'interno della chiamata a reduce, in questo modo:
print reduce(lambda i,j:i+j, numeri)

Analogamente possiamo riscrivere gli esempi con map e filter nel modo seguente:

L = map(lambda i: i, prosciutto)
print L
L = filter(lambda i: (i%2 != 0), numeri)
print L

Altri costrutti

Naturalmente anche se le funzioni descritte sono utili in quanto tali, non sono da sole sufficienti a consentire di usare uno stile completamente funzionale in programmi Python. Anche le strutture di controllo del linguaggio devono essere modificate o sostituite con un approccio di tipo PF. Un modo per ottenere questo scopo consiste nell'utilizzare un effetto collaterale della modalità con cui Python calcola le espressioni booleane.

Valutazione troncata

Poiché Python utilizza una modalità di valutazione delle espressioni booleane detta valutazione troncata, è possibile sfruttare alcune proprietà di queste espressioni. Si ricorda che la valutazione troncata consiste nel valutare l'espressione da sinistra a destra ed interrompendo la valutazione quando non è necessario procedere ulteriormente per ottenere il risultato finale.

Vediamo alcuni esempi specifici:

>>> def VERO():
...   print 'VERO'
...   return 1 # valore booleano VERO
...   
>>>def FALSO():
...   print 'FALSO'
...   return 0 # valore booleano FALSO
...

Abbiamo definito due funzioni che segnalano quando vengono chiamate e che riportano come risultato il valore booleano corrispondente al nome. Adesso le utilizzeremo per esplorare il modo con cui vengono valutate le espressioni booleane:

>>>print VERO() and FALSO()
VERO
FALSO
0
>>>print VERO() and VERO()
VERO
VERO
1
>>>print FALSO() and VERO()
FALSO
0
>>>print VERO() or FALSO()
VERO
1
>>>print FALSO() or VERO()
FALSO
VERO
1
>>>print FALSO() or FALSO()
FALSO
FALSO
0

Notate che la seconda parte di una espressione AND viene calcolata se, e solo se, il valore della prima parte e' VERO. Se la prima parte ha valore FALSO, allora la seconda parte non viene calcolata, in quanto l'espressione complessiva non può risultare vera.

Analogamente in una espressione OR se la prima parte è VERO, allora non c'è bisogno di calcolare la seconda parte dato che l'espressione nel suo insieme risulta vera.

Queste proprietà possono essere utilizzate per riprodurre un comportamento simile alle istruzioni condizionali. Supponiamo ad esempio di avere un frammento di codice come il seguente:

if VERO(): print "E' proprio vero"
else: print "No, e' falso"

Questo può essere sostituito con un costrutto nello stile della PF:

V =  (VERO() and "E' proprio vero") or ("No, e' falso")
print V

Potete provare a modificare l'esempio in modo da utilizzare una chiamata alla funzione FALSO() al posto di VERO().

Utilizzando la valutazione troncata delle espressioni booleane abbiamo trovato un modo di eliminare le classiche istruzioni if/else dai nostri programmi. Ricorderete inoltre che nel capitolo sulla ricorsione abbiamo visto come questo meccanismo può essere utilizzato per eliminare i cicli. Quindi combinando questi due metodi è possibile eliminare tutte le strutture di controllo dai programmi, sostituendole con pure espressioni. Questo è un notevole passo in direzione di uno stile di programmazione puramente funzionale.

Conclusioni

A questo punto potreste domandarvi qual'è lo scopo di tutto ciò. E non sareste i soli. Sebbene la PF sia amata da molti informatici accademici (e spesso dai matematici) i programmatori principianti la usano raramente e spesso in modo ibrido affiancandola allo stile più tradizionale di tipo procedurale quando lo ritengono utile.

Quando è necessario applicare operazioni agli elementi di una lista ed usare map, reduce o filter sembra il modo più naturale per farlo, allora usateli liberamente. Talvolta potrete perfino scoprire che la ricorsione è un approccio più appropriato di un ciclo convenzionale. Più raramente potrete utilizzare la valutazione troncata al posto del tradizionale if/else, specie all'interno di una espressione. Come per tutti gli strumenti di programmazione non lasciatevi fuorviare dalla filosofia, ma usate qualunque strumento vi sembri appropriato per il problema da risolvere. Quanto meno tenete presente che esistono varie alternative!

Infine ricordiamo un ulteriore aspetto del costrutto lambda. Esso trova utile applicazione al di fuori del campo della PF nella definizione di funzioni di gestione degli eventi per la programmazione di interfaccie grafiche. I gestori di eventi sono spesso funzioni assai brevi, talvolta sono semplici chiamate ad altre funzioni con alcuni argomenti predefiniti. In entrambi i casi si può utilizzare una funzione lambda come gestore dell'evento, evitando in tal modo la definizione di tante piccole funzioni separate che occupano lo spazio dei nomi e che sono utilizzate una sola volta ciascuna. Ricordate che l'istruzione lambda riporta un oggetto funzione. Tale oggetto viene passato al widget per essere chiamato quando si verifica l'evento. Se ricordate la definizione di un widget di tipo Button in Tkinter, allora l'istruzione lambda potrebbe essere usata come segue:

def write(s): print s
b = Button(padre, text="Cliccami!", 
           command = lambda : write("Mi hanno cliccato!"))
b.pack()

Naturalmente avremmo potuto ottenere lo stesso risultato semplicemente assegnando un valore di default all'argomento di write() e quindi assegnando write al paramentro command di Button. Ma usare lambda anche in questo semplice caso, ha il vantaggio di poter riutilizzare la funzione write() per qualunque numero di bottoni diversi, passando semplicemente una diversa stringa dalla lambda. Quindi per aggiungere un secondo bottone è sufficiente:

b2 = Button(padre, text="Anche me", 
           command = lambda : write("Hanno cliccato anche me!"))
b2.pack()

Possiamo usare la forma lambda anche con la tecnica "bind", che richiede un oggetto evento come argomento:

b3 = Button(padre, text="Voglio essere cliccato anch'io!")
b3.bind(<Button-1>, lambda ev : write("Cliccato"))

E questo è tutto per ciò che riguarda la Programmazione Funzionale. Se desiderate approfondire l'argomento, sono disponibili molte altre informazioni. Troverete alcune indicazioni nel seguente elenco.

Altre informazioni

Se qualcuno ha da proporre altri suggerimenti può mandare un messaggio e-mail all'indirizzo riportato sotto.


Precedente Successivo Indice
 


Se avete domande o suggerimenti relativi a questa pagina mandate un e-mail all'autore: alan.gauld@yahoo.co.uk o al traduttore italiano: lfini@arcetri.astro.it