Download Documentazione FAQ Aiutaci!!!

Queste brevi note si basano su un articolo di David Mertz (mertz at gnosis.cz), Charming Python 13 [off-site link]. Esse sono state leggermente ampliate e riviste fornendo alcuni concetti formali e togliendo alcune argomentazioni superflue. In queste note si introducono alcuni concetti di base della programmazione funzionale in Python.

Paradigmi di Programmazione

In generale nella maggior parte dei casi di fronte ad un progetto software, si cerca di individuare le metodologie di programmazione al fine di risolvere i problemi mediante i linguaggi di programmazione in maniera razionale, e poi si applica tutto ad essenzialmente tre paradigmi di programmazione:

  • Paradigma imperativo.

  • Paradigma funzionale.

  • Paradigma ad oggetti.

Mentre il paradigma imperativo si basa sul concetto di avanzamento di stato e quello ad oggetti sul concetto di classe, l'elemento chiave nella programmazione funzionale è il concetto di funzione.

Formalmente la programmazione funzionale è uno stile che usa la definizione e l'applicazione di funzioni come concetti essenziali. In questo approccio, le variabili svolgono lo stesso ruolo che hanno in matematica: esse sono la rappresentazione simbolica di valori e, in particolare, servono come parametri di funzioni. l'idea sintattica fondamentale è quella di un´espressione e le funzioni si definiscono a partire dalle espressioni, usando le variabili come parametro.

In questo articolo, vedremo come tali concetti sono espressi in Python, linguaggio di programmazione che supporta tutti e tre i paradigmi. In generale le caratteristiche comuni a tutti i linguaggi di programmazione funzionale sono:

  • Le funzioni sono cittadini (oggetti) di prima classe, ovvero tutto ció che potete fare con i "dati" puó essere fatto con le funzioni (come ad esempio passare una funzione ad un´altra funzione).

  • La ricorsione viene utilizzata come struttura primaria di controllo. A proposito occorre ricordare, che il meccanismo di ricorsione è basato sul principio di induzione matematica, che utilizzato in programmazione funzionale fornisce la possibilità di definire una funzione in termini delle sue stesse applicazioni.

    Il problema di base al fine di terminare il calcolo, quando si usa la ricorsione, è quello di prevedere in maniera oculata dei casi (detti casi base) in cui il valore della funzione si possa determinare senza utilizzare la funzione stessa e garantire che il calcolo prima o poi raggiunga quei casi base. In alcuni linguaggi, nessun altro "ciclo" esiste se non con la ricorsione.

  • l'attenzione particolare sull'elaborazione delle liste. Le liste sono una struttura dati fondamentale e sono spesso usate in maniera ricorsiva su sotto-liste come sostituto per le strutture di cicli.

  • La programmazione funzionale tende preservare la semplicità e lo spirito della notazione matematica, basandosi sulla valutazione delle espressioni (esempio funzioni piú argomenti).

  • La programmazione funzionale scoraggia o disabilita completamente - gli statement -, e lavora invece con la valutazione delle espressioni (per esempio funzioni più argomenti). Nel caso puro, un programma è un'espressione (più le definizioni supportate).

  • La programmazione funzionale si preoccupa di quello che deve essere elaborato piuttosto di come debba essere elaborato.

  • Molta programmazione funzionale utilizza le funzioni di "ordine superiore" (per esempio le funzioni che operano su funzione di funzioni).

I sostenitori della programmazione funzionale tentano di convincerci che tutte queste caratteristiche rendono lo sviluppo del codice più rapido, e il codice stesso diventa più compatto, e meno esposto a errori.

Inoltre, i teorici dell'informatica, della logica e dalla matematica trovano più semplice provare le proprietà formali dei linguaggi funzionali e dei relativi programmi, piuttosto che i linguaggi e i programmi imperativi. Per la dimostrazione della programmazione imperativa si usano le Triple di Hoare, che possono costituire un processo di dimostrazione lungo e complesso, mentre per dimostrare la programmazione funzionale si applica spesso la definizione stessa di funzioni e il principio di induzione matematica, il che rende tutto meno complesso.

Caratteristiche funzionali inerenti Python

Python ha la maggior parte delle caratteristiche di programmazione funzionale elencate sopra sin dal Python 1.0, che sono state migliorate nel successivo Python 2.0.

Gli elementi di base di programmazione funzionale sono le funzioni map(), reduce(), filter(), e l'operatore lambda. In Python 1.x, la funzione apply() risulta comoda per l'applicazione diretta di un valore di ritorno lista di una funzione ad un'altra funzione. Python 2.0 fornisce una sintassi migliorata per questo scopo.

Forse soprendemente, queste poche funzioni (e gli operatori di base), sono quasi sufficenti per scrivere ogni programma Python; specificatamente, gli statement di controllo del flusso (´if´, ´elif´, ´else´, ´assert´, ´try´, ´except´, ´finally´, ´for´, ´break´, ´continue´, ´while´, ´def´) possono essere tutti gestiti in stile funzionale usando le funzioni e gli operatori di programmazione funzionale.

Mentre allo stato attuale eliminare tutti i controlli di flusso in un programma è probabilmente solo utile per entrare in contesto "obfuscated Python" (con il codice che apparira molto simile a Lisp), è doveroso capire come la programmazione funzionale esprime il controllo di flusso con le funzioni e la ricorsione.

Eliminare gli statement di controllo del flusso

La prima cosa a cui pensare nel nostro esercizio di eliminazione è la valutazione "corto circuito" delle espressioni booleane. Questa permette di fornire una versione espressione dei blocchi ´if´/ ´elif´/ ´else´ (assumendo che ciascun blocco chiami una funzione, che è sempre possibile combinare). Ecco come:

Esempio 1. Statement condizionali imperativi e espressioni condizionali funzionali

      #------ Chiamate condizionali "Corto-circuito" in Python -----#
      # Normale controllo di flusso basato sugli statement.

      if <cond1>:   func1()
      elif <cond2>: func2()
      else:         func3()

      # Espressione "corto circuito" equivalente

      (<cond1> and func1()) or (<cond2> and func2()) or (func3())

      # Esempio di espressione "corto circuito" 
      >>> x = 3
      >>> def pr(s): return s
      >>> (x==1 and pr(´one´)) or (x==2 and pr(´two´)) or (pr(´other´))
      ´other´
      >>> x = 2
      >>> (x==1 and pr(´one´)) or (x==2 and pr(´two´)) or (pr(´other´))
      ´two´

La nostra espressione versione delle chiamate condizionali sembrerebbe essere nient'altro che un gioco di parole; tuttavia risulta più interessante quando notiamo che l'operatore lambda può restituire un'espressione. Siccome -come abbiamo visto- le espressioni possono contenere blocchi condizionali attraverso la valutazione "corto-circuito", una espressione lambda è completamente generale nell'esprimere valori di ritorno condizionale. Costruiamo il nostro esempio:

Esempio 2. Lambda con la valutazione corto-circuito in Python

      #--------- Lambda con la valutazione corto-circuito in Python -------#
      >>> pr = lambda s:s
      >>> namenum = lambda x: (x==1 and pr("uno")) \
      ...                  or (x==2 and pr("due")) \
      ...                  or (pr("altri"))
      >>> namenum(1)
      ´uno´
      >>> namenum(2)
      ´due´
      >>> namenum(3)
      ´altri´

Le funzioni come oggetti di prima classe.

Gli esempi sopra hanno già testimoniato che lo status di cittadini di prima classe delle funzioni in Python, ma in un modo sottile. Quando creiamo un -oggetto funzione- con l'operazione lambda abbiamo qualcosa di completamente generale. Come tale, siamo in grado di collegare i nostri oggetti ai nomi "pr" e "namenum", esattamente nello stesso modo in cui potremo aver collegato il numero 23 o la stringa "spam" a quei nomi. Ma proprio come possiamo usare il numero 23 senza collegarlo ad alcun nome (per esempio come argomento di una funzione), possiamo usare l'oggetto funzione che abbiamo creato con lambda senza collegarlo ad alcun nome. Una funzione è semplicemente un altro valore con cui potremmo fare qualcoa in Python.

La cosa principale che facciamo con i nostri oggetti di prima classe, è passarli alle funzioni di programmazione funzionale presenti in Python, cioè map(),reduce() e filter().

Ciascuna di queste funzioni accetta oggetti funzione come suo primo argomento:

Per migliorare la comprensione vediamo alcuni semplici esempi di applicazione delle funzioni appena descritte.

Esempio 3. Data una lista [2,1,2,-1,2] restituire una lista con tutti gli elementi > 0.

    list = [ 2, 1, 2, -1, 2]
    def maggioredi0(x): return (x>0)
    filter(maggioredi0,list)

Esempio 4. Data la lista [-2,-4,4,5,6,10] trovare tutti gli elementi minori di zero, e sommarvi 2.


    list=[-2, -4, 4, 5, 6, 10] 
    def minoredi0(x): return (x<0)
    def plus2(x): return(x+2)
    map(plus2,(filter(minoredi0,list)))

Esempio 5. Data una lista restituisci il valore della somma di tutti i suoi elementi.

    
    list=[-2, -4, 4, 5, 6, 10] 
    reduce(lambda n,m:n+m,list)

Esempio 6. Data la lista [-2,-4,4,5,6,10] trovare tutti gli elementi minori di zero, e restituirne la somma.


    
    list=[-2, -4, 4, 5, 6, 10] 
    def minoredi0(x): return (x<0)
    reduce(lambda n,m:n+m,filter(minoredi0,list))

Notate che combinando queste tre funzioni Python standard di programmazione funzionale, una sorprendente quantita di operazioni di "flusso" possono essere svolte (tutte senza statement, solo espressioni).

Cicli funzionali in Python

Rimpiazzare i cicli è semplice quanto lo è stato rimpiazzare i blocchi condizionali. Il for puó essere direttamente tradotto in map(). Per la nostra esecuzione condizionale, abbiamo bisogno di semplificare i blocchi degli statement a singole chiamate di funzioni:

Esempio 7. Il ciclo ´for´ in maniera funzionale in Python

      for e in lst:  func(e)      # ciclo imperativo
      map(func,lst)               # ciclo funzionale basato su map

Occorre notare che una tecnica simile è disponibile per un approccio funzionale al flusso sequenziale dei programmmi. Cioè la programmazione imperativa consiste principalmente di statement che indicano "fai questo, poi fai quello, e poi fai quell'altra cosa". map() ci permette di fare questo con :

Esempio 8. Azioni sequenziali funzionali in Python

      # creiamo una funzione utility per l'esecuzione;
      do_it = lambda f: f()

      # siano f1, f2, f3 funzioni che svolgono un qualche compito
      map(do_it, [f1,f2,f3])    # applicazione di map sulle funzioni

In generale, il nostro programma principale puó essere interamente un´espressione `map()´ con una lista di funzioni da eseguire per completare il programma. Un´altra caratteristica comoda di una funzione di prima classe è che potete metterla in una lista. Tradurre il ´while´ è leggermente più complicato, ma è ancora possibile direttamente:

Esempio 9. Ciclo while imperativo

      
      # while imperativo
      while <cond>:
          <pre-suite>
          if <break_condition>:
              break
          else:
              <suite>

Esempio 10. Ciclo while in maniera funzionale

      # while realizzato in maniera funzionale
      def blocco_while():
          <pre-suite>
          if <break_condition>:
              return 1
          else:
              <suite>
          return 0

      while_PF = lambda: (<cond> and blocco_while()) or while_PF()
      while_PF()

La nostra traduzione di while richiede ancora un blocco_while() che può esso stesso contenere statement piuttosto che unicamente espressioni. Ma potremmo essere in grado di applicare ulteriori eliminazioni a quelle funzioni (per esempio facendo il corto-circuito di ´if/else´. Inoltre, è difficile che la <condizione> sia utile con i testi usuali, come ad esempio ´while myvar==7´, siccome il corpo del ciclo (per come è stato progettato) non può cambiare nessun valore di variabili (beh, le variabili globali potrebbero essere modificate in ´blocco_ciclo()´). Un modo per aggiungere una condizione più utile è permettere che il "blocco_while()" restituisca un valore più interessante, confronti tale valore per una condizione di terminazione. E´ conveniente illustrare un esempio concreto di eliminazione di statement:

Esempio 11. Ciclo funzionale di ´echo´ in Python


      #---------- Ciclo funzionale di ´echo´ in Python ------------#
      # versione imperativa di "echo()"
      def echo_IMP():
          while 1:
              x = raw_input("IMP -- ")
              if x == ´quit´:
                  break
              else:
                  print x
      echo_IMP()

      # funzione di utilità per "identificare gli effetti-collaterali"

      def stampa_monadica(x):
          print x
          return x

      # versione funzionale di "echo()"
      echo_PF = lambda: stampa_monadica(raw_input("PF -- "))==´quit´ or echo_PF()
      echo_PF()

Quello che abbiamo svolto sopra è un esempio completo che comporta l'I/O da terminale, un ciclo e statement condizionali realizzato con un´espressione pura con ricorsione (infatti un oggetto funzione può essere passato ovunque se lo si vuole). Noi utilizziamo ancora la funzione ´stampa_monadica()´, ma questa funzione è completamente generale, e puo essere riusata in ogni espressione di un programma funzionale che potremmo creare successivamente. Notate che ogni espressione contenente ´stampa_monadica(x)´ valuta la stessa cosa come se avesse semplicemente contenuto ´x´. La programmazione funzionale ha la nozione di un "monade"[1] per una funzione che "non fa nulla, e ha un effetto collaterale nel processo".

Eliminazione degli effetti collaterali

Dopo tutto questo lavoro nel descrivere le caratteristiche funzionali di Python, qualche curioso potrebbe chiedersi: "Perché?". Tutte gli esempi visti possono essere realizzati in maniera imperativa, ma la programmazione funzionale porta a compimento l'eliminazione di effetti collaterali dovuti alle transizioni di stato tipiche del paradigma imperativo. Infatti una percentuale molto ampia degli errori nei programmi è dovuta al fatto che le variabili ottengono valori inaspettati durante l'esecuzione del programma. La programmazione funzionale scavalca questo problema semplicemente non assegnando completamente valori a variabili.

Facciamo un´ulteriore esempio, supponiamo di dover scrivere un programma che abbia come compito stampare una lista di coppie di numeri che sono essi stessi presi da due altre liste. Vediamo prima l'approccio imperativo, che potrebbe essere il seguente:

Esempio 12. Versione imperativa


 # Cicli e sottocicli per il calcolo in maniera imperativa
      xs = (1,2,3,4)
      ys = (10,15,3,22)
      bigmuls = []
      # ...ulteriori linee...
      for x in xs:
          for y in ys:
              # ...ulteriori linee...
              if x*y > 25:
                  bigmuls.append((x,y))
                  # ...ulteriori linee...
      # ...ulteriori linee...
      print bigmuls

Questo programma è di dimensioni ridotte, percui è molto improbabile che ci sia qualcosa che vada per il verso sbagliato. E se il nostro compito fosse quello di includere questo pezzo di codice in un progetto più grande che faccia più cose? Beh, vediamo, infatti possiamo notare le sezioni commentate con "ulteriori linee" sono le porzioni di codice dove gli effetti collaterali probabilmente conducono a bachi. In ciascuno di questi punti le variabili ´xs´, ´ys´, ´bigmuls´, ´x´, ´y´ potrebbere acquisire valori inaspettati nel codice ipotetico abbreviato.

Inoltre, dopo che questo codice viene eseguito, tutte le variabili hanno valori che possono o non possono essere aspettati o voluti dal codice successivo. Ovviamente, l'incapsulazione in funzioni/istanze e l'attenzione allo scoping possono essere usati per salvaguardarvi contro questo tipo di errori. E potete sempre usare ´del' sulle vostre variabili quando avete finito di utilizzarle. Ma in pratica, i tipi di errori segnalati sono comuni.

Un approccio funzionale al nostro esempio elimina sia questi effetti collaterali che gli errori. Una possibile versione funzionale del codice imperativo visto sopra potrebbe essere:

Esempio 13. Versione funzionale dell'esempio precedente


 bigmuls = lambda xs,ys: filter(lambda (x,y):x*y > 25, combine(xs,ys))
      combine = lambda xs,ys: map(None, xs*len(ys), dupelms(ys,len(xs)))
      dupelms = lambda lst,n: reduce(lambda s,t:s+t, map(lambda l,n=n: [l]*n, lst))
      print bigmuls((1,2,3,4),(10,15,3,22))

In questo esempio colleghiamo il nostro oggetto funzione anonimo (´lambda´) a nomi, ma questo non è strettamente necessario. Potremo invece semplicemente includerla nelle definizioni, ma per ottenere una maggiore leggibilità procediamo in questo modo. Inoltre procedendo in questo modo otteniamo una funzione utile da avere, combine(), che produce una lista di tutte le coppie degli elementi presenti in due liste date in input. La funzione ´dupelms()´ fornisce più che altro supporto a combine. Anche se questo esempio funzionale è piu prolisso rispetto alla versione imperativa, una volta che ne considerate il riuso delle funzioni di supporto (dupelms e combine), il nuovo codice in ´bigmuls()´ esso stesso è minore che di quello della versione imperativa.

Il vantaggio reale in questo esempio funzionale è che nel codice nessuna variabile cambia il proprio valore, evitando così i possibili effetti collaterali non voluti, dovuti alle transizioni di stato mediante l'assegnamento classico.

Conclusioni

Queste note hanno dimostrato un semplice utilizzo della programmazione funzionale in Python, e che tale utilizzo puo´ permettere un miglioramento del codice, prevenendo errori tipici della programmazione imperativa. Queste note saranno successivamente ampliate al fine di includere esempi tipici di utilizzo di programmazione funzionale e problemi ben noti risolubili con essa.

Riferimenti

Per coloro che fossero interessati ad approfondire l'argomento, cito i seguenti riferimenti:

Libri

Alcuni libri interessanti relativi alla programmazione funzionale:

Note

[1]

La parola monade ha derivazione filosofica, e indica un´elemento semplice, indivisibile, inesteso, che costituisce il fine ultimo delle cose.