Funzioni e liste

(Puntata precedente)

(Questo post è stato scritto a quattro mani, dalla premiata ditta glipari & juhan)

Prima di proseguire con la panoramica sulla programmazione funzionale, è bene mettere in chiaro due cose. Primo, la programmazione funzionale può essere considerata un approccio alla programmazione indipendente dal linguaggio utilizzato, anche se alcuni linguaggi permettono di esprimere direttamente e in maniera più efficace i tipici costrutti funzionali.

Secondo, osserviamo che la programmazione funzionale sta diventando di moda perché utile per la risoluzione di molti problemi in maniera pratica ed elegante. Per questo motivo, molti linguaggi di programmazione imperativi forniscono anche delle caratteristiche tipiche della programmazione funzionale. Un esempio è il Python, ed infatti in questo post utilizzeremo primariamente questo linguaggio per i nostri esempi.

Oggi ci soffermiamo sulla lista, un tipo di dato fondamentale per tale tipo di programmazione. Lo faremo inizialmente utilizzando il Python, facendo poi i paragoni con altri linguaggi funzionali.

Non vogliamo mica riscrivere il manuale di Python, naturalmente. Ci limiteremo a enunciare le cose fondamentali, rimandando a siti, libri, corsi per i necessari approfondimenti.

Liste

Chi volesse sapere tutto delle liste in Python, sarà meglio dia un’occhiata qui. Una lista è una sequenza finita di dati non necessariamente dello stesso tipo. Ad esempio, una lista di interi, una lista di caratteri, una lista di stringhe, etc. Le liste si definiscono utilizzando le parentesi quadre, dentro cui si mettono gli elementi separati da virgole. Ecco la lista dei primi 10 numeri primi.

ten_primes = [1, 2, 3, 5, 7, 11, 13, 17, 19, 23]

Le liste sono dinamiche: si possono allungare inserendo elementi in coda o ad una certa posizione, oppure scorciare, cancellando elementi. Si possono poi generare con la funzione range(), oppure utilizzando i due punti. Ecco degli esempi:

ten_primes.extend([29, 31, 37]) # aggiunge altri 3 primi in coda
ten_primes.append(41)           # ne aggiunge un'altro in coda
N_items = len(ten_primes)       # restituisce 14 il numero di elementi della lista
tqq = ten_primes[2:5]           # crea una seconda lista contenente soltanto
                                # 3o, 4o e 5o elemento
print ten_primes[:10]           # stampa i primi 10
print tqq[0]                    # stampa il primo elemento di tqq

Naturalmente, si possono fare un sacco di cose strane, tipo definire liste contenenti altre liste.

In Python, le liste possono contenere un po’ di tutto, anche elementi di tipo diverso. In altri linguaggi che hanno un forte controllo sui tipi (come Haskell), invece, le liste devono contenere elementi tutti dello stesso tipo.

Le liste sono uno degli strumenti di base di ogni programmatore funzionale. Una delle caratteristiche delle liste è che le funzioni ricorsive sulle liste si scrivono benissimo. Infatti, una lista può essere definita ricorsivamente come:

  • La lista vuota []; oppure
  • Il primo elemento (chiamato testa della lista) seguito da una lista contenente il resto degli elementi (chiamato coda)

Ad esempio: la lista [1,2,3] può essere vista come:

  • l’elemento 1 seguito dalla lista [2,3], la quale a sua volta è definita come:
  • l’elemento 2, seguito dalla lista [3], la quale a sua volta è definita come:
  • l’elemento 3, seguita dalla lista [], la quale è definita come lista vuota

Complicato? Beh, inizialmente sì, e molto, ma poi uno si abitua a questi contorcimenti mentali, e scrivere algoritmi ricorsivi diventa il suo pane quotidiano.

Vediamo un po’ di scriverne subito uno. Data una lista, vogliamo ottenere una seconda lista contenente solo i numeri pari presenti nella prima. In Python, la testa di una lista l si ottiene con l[0], mentre la coda si ottiene con l[1:]. Ecco un algoritmo ricorsivo:

#!/usr/bin/python
# -*- coding: utf-8 -*-

def pari(l) :
    if l == [] :           # la lista è vuota
        return []          # fine della ricorsione
    elif (l[0] % 2) == 0 : # se la testa è pari ...
        ll = [ l[0] ]      # ... mettila nella nuova lista ...
        ll.extend(pari(l[1:])) # ... e applica la stessa funzione alla coda
        return ll          
    else :                 # se la testa è dispari
        return pari(l[1:]) # scartala, e applica la stessa funzione alla coda

print pari(range(1,10))

Avete bisogno che spieghi nel dettaglio? Penso sia abbastanza chiara: se la testa della lista è un numero pari, il risultato sarà una lista contenente la stessa testa, e il risultato della funzione pari applicato alla coda. Se è dispari, è semplicemente il risultato della funzione pari applicato alla coda. Il risultato stampato a schermo è la lista [2, 4, 6, 8]. Lo so che si poteva fare più semplicemente con un ciclo for, ma i cicli sono fortemente sconsigliati nella programmazione funzionale, ricordate?

Inoltre, notate che non ci sono vere e proprie variabili locali, a parte la lista ll che ho usato solo per chiarezza, ma che può essere completamente eliminata riscrivendo la funzione come segue:

def pari(l) :
    if l == [] :           
        return []          
    elif (l[0] % 2) == 0 : 
        return [l[0]] + pari(l[1:])
    else :                 
        return pari(l[1:]) 

La riga 5 significa “concatena la testa con la lista ottenuta chiamando la funzione pari sulla coda”.

Per gli esperti e per i curiosi: questa versione è un po’ meno efficiente della versione precedente, sappiatelo!

In realtà, ci sono modi più eleganti di scrivere la stessa cosa. Per esempio, in Python si può usare una funzione chiamata filter, che serve appunto per ottenere una lista sottoinsieme, i cui elementi sono selezionati in base a una funzione booleana. Ecco l’esempio:

def isEven(i) : return i % 2 == 0

l = range(1,10)
print filter(isEven, l)

La funzione isEven ritorna True se un numero è pari. La funzione filter applica la funzione isEven a tutti gli elementi della lista l, e tutti quegli elementi per cui isEven è True vengono inseriti nella lista risultato. Quindi, l’output del programmino di cui sopra è ancora [2, 4, 6, 8]. Ma si può fare ancora di meglio! Si può eliminare del tutto la funzione isEven, trasformandola in una lambda function, così:

print filter(lambda i : i%2== 0, range(1,10))

Ma che cos’è una lambda function? Juhan ne aveva già parlato qui. Praticamente è una funzione inlined nel codice. Si elimina il nome (inutile, perché la funzione non verrà utilizzata da nessun’altra parte), e si sostituisce con il generico lambda; si elimina il return e resta solo la lista dei parametri (qui il solo i) e l’espressione. Va bene usarla per robetta semplice, come appunto una funzione con il solo return. In pratica, l’espressione lambda x : expr, è equivalente a scrivere una funzione

def fun x : return expr

e a passarla alla filter.

(Uff, lungo questo post!)
Ma aspettate! Dobbiamo ancora fare un passettino: la list comprehension. Lo stesso esempio si può scrivere così:

print [x for x in range(1,10) if x%2==0]

Che si legge: “stampa l’insieme degli x tali che x appartiene a range(1,10), e solo se x è pari”. Assomiglia un po’ alla notazione matematica per definire un insieme con certe caratteristiche, che nel nostro esempio sarebbe:

\{x | x \in [1, 10) \wedge x \; \mbox{mod} \; 2 = 0 \}

In effetti l’idea è proprio quella: avvicinarsi il più possibile alla notazione matematica.
Sulle liste ci torneremo presto con altre interessanti operazioni, map e reduce su tutte. E poi per fare qualche esempio un tantinello più complicato.

Le liste in NewLISP

E se questo è quello che trovate in Python, provate a immaginare cosa potreste trovare in un linguaggio funzionale! Cominciamo dal NewLISP.

Una lista in NewLISP è una sequenza di elementi tra parentesi tonde. In LISP (e NewLISP di conseguenza) si usano quasi soltanto parentesi tonde, tanto è vero che uno dei classici jokes dice che LISP starebbe per Lista di Inutili Stupide Parentesi! Ma voi non credetegli, non sono poi così stupide!

xckd sul lisp

NewLISP è leggermente diverso dal LISP classico, di cui tra l’altro ne esistono tantissimi altri dialetti. La testa di una lista in newLISP si ottiene con la funzione first (ma dimmi te!), mentre nel LISP serio con car. Il resto della lista si ottiene in newLISP con rest, mentre nel LISP serio con cdr. Adesso vediamo l’esempio precedente in newLISP utilizzando la funzione predefinita filter:

;; definisco even?, torna true per pari, nil per dispari 
(define (even? n) 
    (= (% n 2) 0)
)

;; la lista
(set 'L (sequence 1 9))

;; e filtro
(set 'P (filter even? L))

(println P)
(exit)

Spieghiamo. In NewLISP, le funzioni si definiscono con define (il nome della funzione può contenere il punto interrogativo che non è un carattere speciale). Il nome della funzione è seguito dal nome del parametro n.

Per inciso: notate la notazione prefissa per gli operatori: prima l’operatore, poi gli argomenti. Questo perché gli operatori non sono altro che funzioni che prendono due argomenti! Ad esempio, se dobbiamo fare due più due, in NewLISP scriveremo: + 2 2 (cioè applica la funzione somma agli argomenti 2 2).

Nei linguaggi funzionali è abbastanza comune evitare di usare le parentesi per “calcolare” il valore di una funzione. Ad esempio, se vogliamo calcolare il valore di fun nel punto 5, in un linguaggio imperativo scriveremo fun(5), mentre in un linguaggio funzionale scriveremo semplicemente fun 5. Quindi, si scrive + 2 2, piuttosto che +(2,2).

Tornando al nostro esempio, alla riga 3 l’operatore di confronto = prende due argomenti, il primo dei quali è il risultato dell’applicazione dell’operatore % ai parametri n e 2. Le parentesi servono a raggruppare gli argomenti, in modo che l’interprete NewLISP non si confonda. Torna? E’ tutta questione di abitudine, vedrete.

Poi definisco la lista L. L’operatore set assegna al suo primo argomento L il risultato dell’applicazione della funzione sequence ai parametri 1 e 9. Spero sia chiaro anche questo! Naturalmente, anche NewLISP, che ha a che fare con un sacco di liste, ha la funzione filter, che prende ancora una funzione e una lista. Alla fine, stampiamo a video il contenuto della lista P.

Aspettate, ma non ci saranno mica le funzioni lambda anche in NewLISP? Ma ceeeeerrrrrrrto che si! Ecco una versione con la lambda e con la definizione della lista in linea:

(set 'P (filter (lambda (n) (= (% n 2) 0)) (sequence 1 9)))

(println P)
(exit)

La definizione di lambda è molto simile a quella del Python: lambda seguito da una lista di argomenti, seguito dall’espressione booleana ottenuta applicando l’operatore = etc.
Provate ad eseguire il programma per rendervi conto di come funziona!

Se mi avete seguito fino a qui, siete pronti al salto finale: la stessa cosa in Haskell!
(Argh!!!)

Liste in Haskell

Le liste in Haskell hanno una notazione molto simile a quella di Python. C’è una restrizione: le liste devo contenere elementi tutti dello stesso tipo (o ereditate dallo stesso tipo, come si vedrà più avanti parlando delle gerarchie di tipi). Vediamo alcuni esempi:

[1, 2, 3, 5, 7, 11]  -- alcuni numeri primi
[1..9]               -- tutti i numeri interi da 1 a 9, compresi
l1 : l2              -- l3 è la concatenazione di l1 e l2
head l1              -- la testa di l1
tail l2              -- la coda di l2
(x:xs)               -- x è la testa di una lista, xs è la coda 

I commenti in Haskell cominciano con il doppio meno, –: l’equivalente di range sono i due puntini all’interno della lista (riga 2); per concatenare si usano i due punti (riga 3); le funzioni head e tail restituiscono rispettivamente la testa e la coda di una lista; le quali si possono ottenere anche con la notazione alla riga 5. Quest’ultima notazione sarà subito utilizzata nel programma per ottenere i pari, prima versione, quella ricorsiva:

module Main where
import System.Environment

main :: IO()
main = do args <- getArgs
          putStrLn(show( pari( [1..9])) )

pari :: [Integer] -> [Integer]
pari [] = []
pari (x:xs) = if even x then x : pari xs else pari xs

La prima parte è “difficile” e non siamo ancora pronti a comprenderla a fondo: l’unica cosa da dire è che stampiamo sul terminale con la putStrLn una stringa ottenuta chiamando la funzione show che appunto trasforma il suo argomento in una stringa, e il suo argomento è il risultato della chiamata alla funzione pari sulla lista degli interi da 1 a 9 inclusi.

La funzione pari è dichiarata come una funzione che prende una lista di interi e restituisce un’altra lista di interi. La funzione pari sulla lista vuota ritorna la lista vuota. La funzione pari su una lista con una testa (denominata x) e una coda (denominata xs) ha due casi: se x è pari (chiamamiano la funzione even passandogli x, e questa ci restituisce True se x è pari) allora il risultato è la concatenazione di x con il risultato della funzione pari applicata alla coda: se no è semplicemente il risultato della funzione pari sulla coda.

Finito? Aspetta, si può scrivere anche così:

                                    
pari :: [Integer] -> [Integer]
pari l = [ x | x <- l, even x ]

Stavolta abbiamo usato la list comprehension, che naturalmente è ben presente anche in Haskell! Sembra proprio la notazione matematica: il risultato di pari è una lista di elementi x tali che x appartiene a l, e x è pari.

In Haskell le varie definizioni di funzione vengono chiamate equazioni: nel primo caso abbiamo scritto due equazioni per definire la funzione pari; nel secondo caso ci è stata sufficiente una sola equazione. Sul perché si chiamino equazioni, spero di darvi una risposta più esauriente nei prossimi post! Per ora notate che chi ha inventato il linguaggio ha fatto di tutto per farle sembrare delle equazioni.

Conclusioni

Tiriamo le conclusioni per questo post. Le liste sono un importantissimo strumento per i linguaggi funzionali, perché permettono di scrivere le funzioni ricorsive in maniera semplice e diretta. Tutti i linguaggi funzionali (e non) permettono di manipolare le liste nei modi più vari. Oggi abbiamo visto la funzione filter e le lambda functions in Python e NewLISP; e le list comprehension in Python e Haskell (naturalmente ci sono le lambda function anche in Haskell, anche se non le abbiamo ancora viste). Cosa ci aspetta prossimamente?

  • map e reduce;
  • le liste infinite in Haskell;
  • funzioni pure e costo della ricorsione e del parallelismo nei linguaggi funzionali;
  • qualche programmino leggermente più complicato.

(Continua…)

PS: il plug-in di wordpress per mostrare il codice supporta Python, ma non supporta NewLISP né Haskell. Quindi niente code highlighting per gli ultimi due, ci dispiace!

Posta un commento o usa questo indirizzo per il trackback.

Commenti

  • Mattux  Il 31 ottobre 2011 alle 11:28

    Post interessantissimo e molto chiaro 😀

    Due piccole curiosità:

    – perché la seconda versione della funzione ricorsiva in python è meno efficiente della prima?

    – In NewLISP, quando viene utilizzato l’operatore set, è presente anche un apostrofo che precede il nome della lista (set ‘L …). Perché?

    /*—-*/
    Per l’evidenziazione della sintassi è un peccato effettivamente, in questa riga di Haskell:

    pari (x:xs) = if even x then x : pari xs else pari xs

    Ho avuto qualche problemino a intuirne il significato causa anche l’assenza di parentesi, ma la spiegazione è stata molto chiara.

    Ancora complimenti 😀

    • glipari  Il 31 ottobre 2011 alle 11:45

      La seconda versione è meno efficiente perché l’operatore di concatenazione + ogni volta crea una nuova lista che è l’unione delle precedenti. Invece la funzione membro extend() modifica la lista già esistente, e quindi ci mette un po’ meno e richiede meno memoria. In questo programmino non fa molta differenza, ma con liste molto grandi lo fa eccome.

      Per il simbolo di “quote”: mi dice Juhan che è necessario per dire alla set che stiamo impostando una nuova variabile che si chiama L (o P). Io questa cosa del “quote” in Lisp non l’ho mai capita veramente però! Magari vado a vedere i miei vecchi appunti di Lisp e ti do una spiegazione migliore.

      • Mattux  Il 31 ottobre 2011 alle 11:55

        Ora è tutto chiarissimo, grazie. Per quanto riguarda la set era giusto una curiosità, cioè perché non semplicemente ” set L ” ma ” set ‘L “? Vabbè non importa, pensavo ci fosse qualche tipo di distinzione, grazie ancora 😀

  • glipari  Il 31 ottobre 2011 alle 12:38

    @Mattux: ecco una lunga spiegazione sul quote in Lisp:
    http://stackoverflow.com/questions/134887/when-to-use-quote-in-lisp
    Guarda la prima risposta illuminante.
    (su rete si trova sempre di tutto!).

    • Mattux  Il 31 ottobre 2011 alle 12:48

      Finalmente è tutto più chiaro.
      Sembrerò noioso ma: grazie! 😀

  • juhan  Il 31 ottobre 2011 alle 13:56

    Io stavo quasi pensando di fare un post tipo “Forse non tutti sanno che” della Settimana Enigmistica (chissà se c’è ancora?) dove racconterei delle cose tipo il quote e il ? dei predicati e degli * nelle variabili globali.
    Poi se mi lascio andare ci potrei aggiungere, compreso nel prezzo, le variabili dinamiche. Ma, pensandoci bene, solo se il dr.prof. ci spiega auto del C++11, con l’aiuto di H.Sutter, volendo.
    Però ormai la storia di ‘ sembra chiarita, o no 8)

    • Mattux  Il 31 ottobre 2011 alle 14:12

      Sì che bello *_*

      (Anche se la storia di ‘ è chiarita, spiegarlo per bene in un post sarebbe un ottima cosa.)

    • glipari  Il 31 ottobre 2011 alle 14:36

      ah sì, la settimana enigmistica c’è ancora e anche la rubrica! (la storia di auto è molto semplice, ma un post-it si può sempre fare).

  • juhan  Il 31 ottobre 2011 alle 18:15

    Prossimamente…
    intanto io continuo a alzarmi alla solita ora, cioè un ora prima e adesso sono completamente andato, alle 6 del pomeriggio!
    Nei ritagli di tempo ho abbozzato una cosa che interessava Bl@ster e un suo amico, poi verranno le parentesi, le quote, &co.: è la prima volta che il Lisp non provoca rigetto, forse i tempi stanno cambiando 😉

Trackback

Rispondi

Inserisci i tuoi dati qui sotto o clicca su un'icona per effettuare l'accesso:

Logo WordPress.com

Stai commentando usando il tuo account WordPress.com. Chiudi sessione /  Modifica )

Google photo

Stai commentando usando il tuo account Google. Chiudi sessione /  Modifica )

Foto Twitter

Stai commentando usando il tuo account Twitter. Chiudi sessione /  Modifica )

Foto di Facebook

Stai commentando usando il tuo account Facebook. Chiudi sessione /  Modifica )

Connessione a %s...

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d blogger hanno fatto clic su Mi Piace per questo: