CancelloTorniamo sulla terra. Lasciamo per un momento tutti i concetti “teorici” e ingegneristici della programmazione Scheme per tornare ad alcuni aspetti pratici.

E’ importante, in qualunque linguaggio di programmazione, saper gestire abilmente l’input/output, sia quello “utente” sia quello “file”. Soprattutto in scheme, linguaggio con particolari vocazioni per il data-mining, saper leggere escrivere file diventa fondamentale.

Per leggere flussi dati in ingresso o in uscita Scheme utilizza le porte.

Le porte sono canali connessi alle varie periferiche di uscita/ingresso del sistema. Possono essere usate sia per leggere o scrivere su stdin/stdout (come scanf e printf in C) sia per leggere da file.

CREARE UNA PORTA

Creare una porta è semplicissimo:

1
2
(open-input-file str) ;Per un file in lettura.
(open-output-file str) ;Pre un file in scrittura.

Ovviamente al posto di str ci va il percorso del file che si vuole aprire.

LEGGERE

Per leggere si utilizza semplicemente il comando:

1
(read port)

Dove port è la porta che abbiamo aperto in precedenza.

Il comando

1
(eof-object port)

ci dice, invece, se abbiamo raggiunto la fine del file.

NOTA: Se nessuna porta è specificata Scheme leggerà dallo standard input (terminale).

SCRIVERE:

Analogamente alla lettura Scheme offre i seguenti comandi per scrivere:

1
2
3
(write obj port)    ;Scrive l'oggetto obj sulla porta port
(display obj port) ;Mostra l'oggetto obj attraverso la porta port
(newline port) ;Scrive una linea vuota sulla porta port

Come nel caso della lettura anche qui, se non specifichiamo la porta, Scheme effettuerà la scrittura sullo standard output (ovvero sul terminale).

Questa lezione termina quì. L’uso delle porta in Scheme è veramente molto semplice. Appena posso posterò degli esempi.

Alla prossima.

 

frattaleDopo aver visto come funziona la ricorsione in Scheme dobbiamo valutare un altro aspetto molto rilevante: il costo. Non tutto ciò che logicamente funziona funziona poi all’atto pratico. Ad esempio, se ripescate la funzione ricorsiva della Funzione di Fibonacci e provate a farla partire noterete che già con numeri relativamente bassi (come 100) il sistema inizierà a intasarsi e non darà alcun risultato se non quello di dover killare il processo.

Perché questo? Perché l’implementazione dell’algoritmo, sebbene sia vera e matematicamente corretta, necessita un numero troppo elevato di calcoli o di memoria.

Iniziamo col dare dei cenni sulla complessità di un algoritmo. La complessità di un algoritmo è definita asintoticamente con un infinito di ordine superiore a una funzione f(x). In questo caso si dice che l’algoritmo ha complessità dell’ordine di f(x).

Cosa significa? E’ semplice. Supponete di far valere 1 il costo di ogni operazione base come somma, prodotto, cdr, car e via dicendo (ovviamente è un approssimazione). Supponete di stabilire un contatore che sommi il costo di ogni operazione eseguita. Supponete poi di eseguire questa funzione (con relativo contatore) per un imput di dimensione 1, poi per un input di dimensione 2, poi per un input di dimensione 3 e così via. La serie di numeri che otterrete non sarà altro che i valori di una funzione f(x)al variare di x. La complessità si riduce quindi all’ordine di infinito di questa funzione.

Data l’asintoticità della definizione, solitamente si trascurano i valori per x piccolo e si approssima molto per x che tende a infinito.

Senza addentrarci nei particolari sappiate che in ogni algoritmo esiste un operazione, detta operazione dominante, la quale complessità è uguale alla complessità dell’algoritmo nella sua interezza.

Negli algoritmi ricorsivi l’operazione dominante è la ricorsione. Per trovare la complessità di una funzione in scheme (che sono per definizione tutte ricorsive) ci basta quindi sapere quante volte una funzione chiama la ricorsione.

Questo può essere molto complicato, ma è giusto che impariate a identificare almento i casi semplici. Questo può essere fatto molto semplicemente.

Supponiamo di voler calcolare la complessità dell’algoritmo fattoriale visto qualche lezione fa. L’espressione ricorsiva del fattoriale è la seguente:

FattorialePer trovare il costo basta seguire la formula:

CodeCogsEqn(2)Questo perché il costo di f(0) è chiaramente unitario. Il costo di f(n), per il principio dell’operazione è solamente il costo della ricorsione e il costo della ricorsione non è altro che 1 più il costo di tutte le altre funzioni invocate.

Ma siamo arrivati ad una suluzione? Ancora no. Dobbiamo esplicitare il costo ovvero toglierlo dalla sua forma ricorsiva. Per fare ciò possiamo semplicemente espandere la seconda equazione.

  • C[f(n)]
  • 1 + C[f(n-1)]
  • 2 + C[f(n-2)
  • ...
  • n + C[f(0)]
  • n+1
  • che è asintoticamente O(n)

Il costo è quindi lineare. (Non esattamente… ma per non mettere troppa carne al fuoco farò alcune precisazioni al riguardio nella prossima appendice).

Una funzione con complessità lineare è solitamente una funzione discretamente veloce e potente.

Vediamo ora la stessa cosa con la Funzione di Fibonacci.

Costo Fibonacci

La situazione è leggermente più complessa. Per esplicitare la funzione possiamo comunque espandere C[f(n)] ma non conviene in questo caso. Facciamo un ragionamento più sottile. Cerchiamo di calcolare direttamente il costo totale. Sappiamo che ogni ricorsione chiama due ricorsioni. Alla prima ricorsione avremo costo totale 1, alla seconda avremo il costo totale di prima più il costo delle due nuove ricorsioni ovvero 1 + 2. Alla terza avremo il costo di prima  più le due ricorsioni nuove lanciate da ognuna delle due ricorsione precedenti ovvero 1 + 2 + 4 (il costo di prima più le due ricorsioni nuove lanciate da ognuna delle due ricorsione precedenti).

Notiamo che abbiamo una serie geometrica che possiamo riassumere in 2^n+1 – 1 che asintoticamente è O(2^n).

Il costo è esponenziale. Un algoritmo esponenziale può solitamente calcolare solo input molto piccoli. Ecco perché il nostro algoritmo di Fibonacci si bloccava anche con numeri piccoli come 100.

Come possiamo allora migliorare questo algoritmo? Esistono tecniche un po’ più elaborate che permettono di eseguire algoritmi simili lanciando solo una ricorsione al posto delle due “canoniche”. Queste tecniche le esploreremo più in là in quanto necessitano di un po’ di esperienza.

Per il momento vi basti intuire che algoritmi che eseguono troppe ricorsioni contemporaneamente sono spesso troppo costosi per essere utili. Infatti abbiamo visto che basta lanciare due ricorsioni in vece di una per fare esplodere il costo da lineare ad esponenziale.

Non vi serve saper calcolare la complessità di tutto ma vi basta intuire quando un algoritmo sta diventando troppo costoso. Questo è basilare per ogni programmatore, anche al di là di Scheme.

Provate a calcolare il costo di altre funzioni di esempio che ho dato in precedenza. Postate il costo che avete ottenuto nei commenti e vedrò di verificare se avete indovinato.

 

Escher - Mani che Disegnano se StesseNella scorsa lezione abbiamo visto come funziona la semplice ricorsione applicata a funzioni numeriche. La ricorsione è però molto più potente e ci permette di operari anche con oggetti che non siano solamente numeri.

Diamo quindi ora una definizione di algoritmo ricorsivo meno formale ma che serve a decifrare meglio il problema a livello pratico.

Supponiamo che ci sia una funzione F che risolva un problema su un input P di dimensione N. N può essere il numero di elementi di una lista, il numero di nodi di un grafo o di un albero, il numero di byte in un file e qualunque altra cosa. Chiamiamo P-N un qualunque ingresso di dimensione N e chiamiamo F(P-N) l’algoritmo che opera sull’input P. Inoltre definiamo con K un qualunque P fissato (ad esempio una lista vuota, oppure una lista con 2 elementi, oppure un albero senza sotto-albero destro, etc…

Da questo ricaviamo che un algoritmo ricorsivo è tale se e solo se:

  • F(K) ha un risultato immediato (passo base)
  • F(P-N) può essere calcolato immediatamente conoscendo il risultato di F(P-M) dove M è minore di N. (passo ricorsivo)

Questo cosa vuol dire. Vuol dire che un algoritmo ricorsivo ricava il risultato su un input di dimensione N grazie al risultato che ottiene su un sotto insieme dell’input iniziale di dimensione M.

Come al solito l’esempio chiarisce molto più di mille parole. Supponiamo di voler scrivere l’algoritmo ricorsivo per invertire una lista ovvero mettere in testa l’ultimo elemento, in seconda posizione il penultimo e così via.

1
2
3
4
5
(define (rovescia lista)
  (cond ((null? lista) '())
      (else (append (rovescia (cdr lista)) (list (car lista))))
  )
)

L’algoritmo non fa altro che appendere l’elemento di testa della lista in coda alla sottolista rovesciata. Ovvero, supponiamo di avere la lista (1 2 3 4) e di volerla rovesciare. Prendendo il primo elemento della lista avremo (1) e la sottolista (2 3 4). Supponiamo di avere la sottolista già rovesciata, ovvero (4 3 2). Ci accorgiamo che per avere la lista originale rovesciata ci basta prendere (1) e metterlo in coda a (4 3 2).

Il problema si riduce quindi a rovesciare (2 3 4). Ma questo a sua volta si riduce a rovesciare (3 4). Che a sua volta si riduce a rovesciare (4). Ma (4) è già rovesciato. E quindi abbiamo concluso.

Ecco un altro esempio:

1
2
3
4
5
6
(define (presente x lista)
   (cond ((null? lista) #f)
      ((= (car lista) x) #t)
      (else (presente x (cdr lista)))
   )
)

Questo algoritmo verifica se l’elemento x è presente nella lista lista. Ancora più semplicemente possiamo applicare il seguente ragionamento ricorsivo:

  • Se x è il primo elemento della lista allora l’elemento x è presente. (caso ase)
  • Se la lista è vuota allora l’argomento non è presente. (caso base)
  • Altrimenti verifico se è presente nel resto della lista. (caso ricorsivo)

Questo è un esempio di ragionamento ricorsivo. In pratica si sposta il problema ad un qualcosa di più piccolo rispetto all’input originale.

Come ultimo esempio vi cito questo:

1
2
3
4
5
(define (sommalista lista)
  (cond ((null? lista) 0)
      (else (+ (car lista) (sommalista (cdr lista))))
  )
)

L’algoritmo non fa altro che sommare tutti gli elementi di una lista. Anche in questo caso il ragionamento ricorsivo è semplice.

  • Se la lista è vuota la somma è ZERO. (caso base)
  • Altrimenti la somma è uguale al primo elemento più la somma del resto della lista.

Torneremo a parlare di ricorsione ancora e ancora. Cerco di fare il possibile per trattarla in modo comprensibile nonostante gli spazi “ristretti” di un blog.

La prossima lezione affronterà un tema molto importante. Il costo della ricorsione. Cercheremo di trovare un modo semplice per capire quanto costa a livello di prestazioni un algoritmo ricorsivo e come evitare che diventi troppo esoso.

 

Escher-RecursionOra che conosciamo tutte le basi di Scheme dobbiamo iniziare la parte più difficile, abituare la nostra testa a ragionare secondo il modello funzionale. Per fare questo non possiamo prescindere dall’uso di un linguaggio un po più matematico ma che, vi assicuro, dopo l’impatto iniziale si rivelerà piuttosto automatico (e se avete dubbi potete sempre chiedere su LQH).

La base di scheme, come sapete, è funzionale. In quanto linguaggio funzionale esso va programmato secondo i canoni della teoria delle funzioni ricorsive. Le funzioni ricorsive sono un argomento molto esteso ma di importanza fondamentale nella teoria della calcolabilità. In questa lezione non ci occuperemo della totalità delle funzioni ricorsive ma ci limiteremo a trattare il sottoinsieme delle funzioni ricorsive primitive. Queste funzioni ricorsive hanno, fra le tante, la non trascurabile proprietà di terminare sempre. Ovvero data una qualunque funzione ricorsiva primitiva e un qualsiasi input la funzione cesserà la propria esecuzione restituendoci un risultato.

Senza addentrarci troppo nella teoria sappiate che una funzione ricorsiva è una funzione nella quale esiste un caso in cui la soluzione è diretta e nel caso generale il risultato è ricavabile passando ad un’altra funzione tutti gli argomenti della funzione ricorsiva più il risultato della stessa calcolato su un insieme più piccolo degli argomenti originali. Ovvero:

Detto così sembra enormemente complicato. In realtà nella maggior parte dei casi una funzione ricorsiva può essere riportata alla ricorsione di una funzione ad un argomento e quindi.

CodeCogsEqnCodeCogsEqn(5)

Se anche così la cosa vi crea problemi cercherò di risolverli con il più semplice degli esempi: il fattoriale. Ricordate inoltre che la funzione g(y,x) solitamente è implicita.

Il fattoriale espresso ricorsivamente apparirà come:

CodeCogsEqn(3)CodeCogsEqn(4)

Che penso sia di facile interpretazione.

I casi in cui risaliamo direttamente al risultato si chiamano casi base, i casi in cui è necessario usare la ricorsione sono detti casi ricorsivi.

Poiché questo è un corso di Scheme vediamo come trasportare questa funzione matematica in una funzione di Scheme. Il passo è semplice.

1
2
3
4
5
(define (fattoriale y)
    (cond ((= y 0) 1)
    (else (* y (fattoriale (- y 1))))
    )
)

Come vedete ci basta inserire in un COND tutti i casi base e tutti i casi ricorsivi. Il caso base, in questo esempio, equivale a y=0, in questo caso infatti il fattoriale è semplicemente 1. Se y non è nulla allora dobbiamo eseguire il passo ricorsivo. Notate che in questo caso la funzione g(y,x) è semplicemente la funzione prodotto.

Il meccanismo di funzionamento è semplice. Le chiamate ricorsive hanno sempre due stati:

  • Uno stato di crescita in cui si fanno le chiamate ricorsive inserendo tutto sullo stack.
  • Uno stato di calcolo in cui partendo dalla cima dello stack si effettuano i calcoli ripercorrendo le varie chiamate ricorsive a ritroso.

Nel caso di funzioni ricorsive semplici questi stati sono raggruppati in due fasi distinte. Lo stato di crescita continua fino al raggiungimento di un caso base dopo il quale tutte le chiamate sullo stack terminano a cascata.

Facciamo un esempio. Riprendiamo la nostra funzione fattoriale e vediamo come si comporta nel fare il fattoriale di 4.

La prima chiamata sarà

(fattoriale 4)

A questo punto si entra nel COND. Y è chiaramente diverso da zero, quindi viene eseguito il ramo else. La funzione esegue:

(* 4 (fattoriale 3) )

A questo punto viene invocato nuovamente fattoriale. L’esecuzione corrente viene quindi sospesa mentre viene avviata una nuova esecuzione di fattoriale. A questo punto i passi vengono rieseguiti nella nuova chiamata. Y è ancora diverso da zero quindi viene eseguito ancora il ramo else.

( * 4 ( * 3 (fattoriale 2) ) )

Viene aggiunta una nuova chiamata in pila. E così via.

( * 4 ( * 3 ( * 2 ( * 1 (fattoriale 0) ) ) ) )

A questo punto però ci troviamo nella situazione in cui la nuova chiamata di fattoriale è esattamente il caso base. A questo punto la chiamata termina restituendo 1 senza eseguire una nuova chiamata a fattoriale.

( * 4 ( * 3 ( * 2 ( * 1 1 ) ) ) )

Inizia così la fase di calcolo e distruzione. Terminata l’ultima chiamata di fattoriale si riprende dalla chiamata precedente che non fa altro che calcolare 1 x 1 e terminare. E così via a cascata fino alla chiamata originale che termina con il risultato complessivo. Ovvero 24.

Questo è il meccanismo generale di una funzione ricorsiva.

RICAPITOLANDO

  • Una funzione ricorsiva è una funzione che richiama se stessa su una porzione più piccola degli argomenti.
  • Una funzione ricorsiva primitiva termina sempre.
  • Una funzione ricorsiva primitiva è sempre composta da uno o più casi base e uno o più casi ricorsivi.
  • Una funzione ricorsiva senza casi ricorsivi non è ricorsiva. Una funzione senza casi base non termina mai.
  • Una funzione ricorsiva presenta nella sua esecuzione due stati: uno stato di crescita e uno stato di calcolo e distruzione.

RISORSE

Allego di seguito un file Scheme che contiene alcune piccole funzioni ricorsive numeriche. Potete dargli un occhiata, prima però potete provare a farle da voi. Le funzioni sono:

  • Fattoriale (fatto)
  • Serie di Fibonacci
  • Moltiplicazione
  • Somma
  • Elevazione a potenza

Il file lo trovate QUI.

Nella prossima lezione continueremo questo argomento cercando di chiarire molti altri aspetti pratici di una funzione ricorsiva e vedremo come applicarla a compiti più complessi del semplice calcolo delfattoriale di un numero.

 

MatematicaMettiamo una pausa alle lezioni per mostrare un elenco piuttosto esaustivo delle funzioni di utilità generale che possiamo richiamare durante la programmazione.

Le funzioni, come abbiamo già visto, sono tutte in notazione prefissa. Inoltre, a differenza di altri linguaggi, Scheme possiede molte funzioni matematiche, anche complesse, di default, senza dover importare nessun modulo o libreria.

Un altra differenza con le funzioni di altri linguaggi consiste nel poter calcolare direttamente un gran numero di argomenti. Queste funzioni hanno quindi un numero di parametri potenzialmente infinito. La funzione “addizione”, per esempio, prende un numero qualunque di parametri e li sommatutti mentre con la notazione ordinaria (a + b) possiamo sommare soltanto due elementi alla volta.

Vediamo quindi una prima lista di funzioni. Sono funzioni numeriche ovvero che lavorano con numeri.

Funzioni Risultato
(+ arg1 arg2 ... argN)
Somma tutti gli argomenti.
(- arg1 arg2 ... argN)
Sottrae ad arg1 tutti gli argomenti.
(* arg1 arg2 ... argN)
Moltiplica tutti gli argomenti.
(/ arg1 arg2 arg3... argN)
Divide ad arg1 tutti gli argomenti.
(log arg)
Logaritmo naturale di arg
(exp arg)
Esponenziale di arg
(sin arg)
Seno di arg
(cos arg)
Coseno di arg
(tan arg)
Tangente di arg
(asin arg)
Arcoseno di arg
(acos arg)
Arcocoseno di arg
(atan arg)
Arcotangente di arg
(sqrt arg)
Radice quadrata arg
(expt base potenza)
Eleva la base alla potenza
(abs arg)
Valore assoluto di arg
(quotient arg1 arg2)
Restituisce la parte intera di arg1 / arg2
(modulo arg1 arg2)
Resto della divisione
(ceiling arg)
Approssimazione della parte intera per eccesso
(floor arg)
Approssimazione della parte intera per difetto
(round arg)
Approssimazione generica di arg
(truncate arg)
Troncamento di arg
(max arg1 ... argN)
Valore massimo tra arg1 e argN
(min arg1 ... argN)
Valore minimo tra arg1 e argN
(gcd arg1 ... argN)
Massimo comune divisore tra arg1..e .. argN
(lcd arg1 ... argN)
Minimo comune multiplo tra arg1.. e .. argN
(numerator arg)
Numeratore di arg
(denominator arg)
Denominatore di arg

Inoltre sono disponibili queste funzioni di utilità:

Funzioni Domanda
(zero? numero)
Il numero è uno zero?
(positive? numero)
Il numero è positivo?
(negative? numero)
Il numero è negativo?
(odd? numero)
Il numero è dispari?
(even? numero)
Il numero è pari?

Queste tabelle sono state tratte da Wikipedia con qualche correzione e ampliamento personale.

 

BivioCome in ogni linguaggio di programmazione, una delle cose essenziali per scrivere un qualunque programma è la  gestione delle condizioni. Una condizione consiste, come penso sappiate, nell’eseguire un blocco di comandi a seconda se sia rispettata o meno una data condizione.

In Scheme la gestione delle condizioni è molto completa. Iniziamo quindi a vedere uno dei costrutti più famosi anche se in Scheme non viene molto usato: l’if.

IF

1
2
3
(if condizione
    espressione_nel_caso_la_condizione_sia_vera
    eventuale_espressione_nel_caso_la_condizione_sia_falsa)

Questa è la struttura dell’if. Viene valutata condizione e a seconda se sia vera o falsa viene eseguito rispettivamente il primo o il secondo blocco di espressioni. Guardiamo un piccolo esempio:

1
2
3
4
5
6
7
; TESTIF: Verifica se A è uguale a Zero.
(define (testif a)
 (if (= a 0) (display "Il valore inserito è uguale a zero.")
         (display "Il valore inserito NON è uguale a zero.")
 )
 (newline)
)

Questa funzione non fa altro che dire se il valore passato per parametro è zero oppure no. Potete subito constatare che questo costrutto è del tutto simile agli if presenti negli altri linguaggi.

COND

COND è il costrutto condizionale più utilizzato a causa della sua versatilità. Esso è infatti in grado di simulare abilmente sia un IF sia un CASE. Vediamo come è strutturato.

1
2
3
4
5
(cond
(prima_condizione espressione)
(seconda_condizione espressione)
...
(else espressione))

Ad ogni sua applicazione vengono valutate le condizioni a partire dalla prima fino all’ultima e viene eseguito il blocco di istruzioni corrispondente alla condizione vera. Se nessuna condizione è vera viene eseguito il ramo else. Vediamo un esempio:

1
2
3
4
5
6
7
8
; TESTCOND: Verifica se B è uguale a 0 o a 1.
(define (testcond b)
    (cond ((= b 0) (display "B = 0"))
        ((= b 1) (display "B = 1"))
        (else (display "NONE"))
    )
    (newline)
)

Questa funzione verifica se il parametro è 0 oppure 1 e in caso contrario risponde con NONE.

CASE

Questo è l’ultimo costrutto decisionale in Scheme. Si comporta in modo molto simile allo switch…case presente in molti altri linguaggi.

1
2
3
4
5
(case espressione_che_viene_valutata
   ((val1) espressione)
   ((val2) espressione)
   ...
   (else espressione))

Viene innanzitutto valutata l’espressione. Dopodichè il valore risultante viene confrontato con la lista di valori val1, se il valore è contenuto in val1 viene eseguita l’espressione corrispondente altrimenti si passa a val2. Così via fino a else che viene eseguita in ogni caso.

1
2
3
4
5
6
7
8
9
; TESTCASE: Verifica se C è pari o dispari compreso fra 0 e 10.
(define (testcase c)
 (case c
     ((1 3 5 7 9) (display "C = DISPARI"))
     ((2 4 6 8 10) (display "C = PARI"))
     (else (display "NONE"))
 )
 (newline)
)

RICAPITOLANDO:

  • IF esegue un espressione se la condizione è vera, ne esegue un altra se è falsa.
  • COND esegue l’espressione corrispondente alla condizione vera o a else se nessuna è vera.
  • CASE calcola un espressione ed esegue i comandi associati alla lista che contiene tale valore.

 

ForbiciContinuiamo il nostro studio di Scheme analizzando un altro elemento fondamentale della programmazione Lisp: la manipolazione delle liste. Come abbiamo visto in precedenza in Scheme tutto ciò che non è un atomo (numeri, stringhe, caratteri e altri tipi base) è una lista, appare quindi chiara l’importanza che riveste la loro manipolazione nella programmazione funzionale.

Per fare ciò analizzaremo quella manciata di comandi che ci permettono di fare tutto.

CAR e CDR

Questi sono i due comandi principali. Grazie a questi comandi possiamo già fare praticamente tutto. I comandi che vedremo successivamente possono benissimo essere ricreati usando solo CDR e CAR (e CONS). Nei nostri esempi considereremo una lista di numeri (1 2 3 4 5 6).

Tengo a precisare che indicare una lista così:

'(1 2 3 4 5 6)

è del tutto equivalente all’uso del comando list.

(list 1 2 3 4 5 6)

EFFETTI:

  • CAR estrae il primo elemento della lista passata per argomento.
    ESEMPIO:

    (car '(1 2 3 4 5 6)) restituisce 1
  • CDR ritorna la lista formata dalla lista passata per argomento senza il primo elemento.
    ESEMPIO:

    (cdr '(1 2 3 4 5 6) restituisce (2 3 4 5 6)

Questi comandi inoltre godono di una proprietà particolare. Possono essere composti a piacimento inserendo una A per ogni CAR e
una D per ogni CDR nella parola C*R. Detto così sembra complicato, in realtà è però molto semplice. Facciamo un esempio.

  • (CADR lista) equivale a (CAR (CDR lista)).
  • (CADDR lista) equivale a (CAR (CDR (CDR lista))).
  • (CDADR lista) equivale a (CDR (CAR (CDR (CAR lista)))).

In queste composizioni è però necessario considerare il risultato di ogni applicazione. Sia CAR che CDR prendono come argomento una lista ma mentre CDR restituisce sempre una lista CAR restituisce una lista SE E SOLO SE la lista che gli viene data è una lista di liste.

Questo è causa di errori. Infatti non si può fare il CAR/CDR di un elemento.

Confusi? E’ normale. Ci vuole un po per afferrare questi concetti ma risulteranno molto intuitivi dopo un po di applicazioni.

CONS

Cons è un altro comando essenziale. Questo comando la cui sintassi è

(CONS arg1 arg2)

Non fa altro che inserire in testa alla lista arg2 l’elemento arg1.

APPEND

Append è un altro comodo comando la cui sintassi è identica a quella di CONS.

(APPEND arg1 arg2)

Questo comando però serve a concatenare due liste dando come risultato una lista contenente prima tutti gli elementi di arg1 e poi tutti gli elementi di arg2.

REVERSE

Ultimo comando che userete spesso è Reverse. Questo comando inverte semplicemente l’ordine degli elementi all’interno della lista. Ad esempio:

(REVERSE '(1 2 3)) restituisce (3 2 1)

RICAPITOLANDO

  • CAR estrae il primo elemento di una lista.
  • CDR restituisce la lista senza il primo elemento.
  • CAR si applica solo a LISTE e il suo risultato è un ELEMENTO o una LISTA
  • CDR si applica solo a LISTE e il suo risultato è sempre una LISTA.
  • CAR e CDR possono combinarsi in comandi come CADR o CADDDDR in cui ogni A indica un CAR e ogni D indica un CDR.
  • CONS inserisce un elemento in testa ad una lista.
  • APPEND concatena due liste.
  • REVERSE inverte l’ordine degli elementi di una lista.

ESERCIZI E SPUNTI DI RIFLESSIONE

Lascio qui di seguito alcuni “esercizi” per mettere alla prova la comprensione di questa lezione.

  • Esprimere in un comando CAR/CDR una funzione che estrae il secondo elemento della seconda lista di questa lista ( (0 2) (4 3 2) (9 9 9) 9 ).
  • Date le due list LIST1 = (1 2 3) e LIST2 = (4 5 6) riflettere sulla differenza fra (CONS LIST1 LIST2) e (APPEND LIST1 LIST2).

La prossima volta vedremo come affrontare decisioni e condizioni. Comincieremo cioè a scrivere le prime funzioni complete e funzionali.

 

LambdaScheme è uno dei principali dialetti del Lisp. E’ un linguaggio multi paradigma anche se incentrato sul paradigma funzionale per questioni ereditarie. Nonostante ciò con Scheme si può fare praticamente tutto, dalla programmazione a oggetti alla programmazione imperativa, dalla funzionale ricorsiva alla sua versione “logica”.

Ci tengo però a precisare che Scheme è e rimane un linguaggio un po particolare. Diverso concettualmente da tutti i linguaggi tradizionali. Scheme è indirizzato a:

  • Persone che sanno programmare e che vogliono espandere i loro confini in modi che non avrebbero mai immaginato.
  • Persone che non hanno mai programmato e che volgiono conoscere lo spirito intimo della programmazione.
  • Persone a cui la programmazione classica non è mai piaciuta ma amano lo spirito “matematico” della stessa.
  • Persone che amano la neuroingegneria in senso lato.

A chi NON è indirizzato:

  • Persone che sanno programmare e che non sentono il bisogno di imparare altro.
  • Persone che non hanno mai programmato e vogliono subito essere produttivi e fare qualche applicazione carina.
  • Persone la cui idea di programmazione è costruire enormi gestionali in grado di mantenere informazioni su pescherie sul mar baltico, parrucchieri per donnole, palestre e altre cose noiose (per me ovviamente).

Chiariti questi punti spero di avere da ora in poi solo persone che rientrano nei primi tre punti e che non si spazientiscano se Scheme sembri strano, complicato, astratto e chi più ne ha più ne metta.

Iniziamo con il presentare alcuni aspetti essenziali. Lo studio di Scheme è IMPRESCINDIBILE da questi concetti.

NOTAZIONE PREFISSA

Scheme come in Lisp esprime tutto in notazione prefissa. In notazione prefissa bisogna prima specificare la funzione e poi tutti i suoi parametri a seguire.

Ad esempio 1+1 diventa:

(+ 1 1)

Mentre 2 + (3 * 2) + 1 diventa:

(+ 2 (* 3 2) 1)

Questo tipo di notazione può risultare a prima vista molto complessa e inutilmente dispendiosa. E’ vero e non è vero. Nelle semplici operazioni matematiche può effettivamente risultare poco intuitiva e poco chiara, tuttavia, in tutti gli altri casi questa forma permette un controllo del codice inqguagliabile. E’ molto facile, ad esempio, istruire un programma a creare ed eseguire codice proprio.

LISTE E ATOMI

Un altra cosa da tenere in considerazione è che in Scheme tutto è una lista. Anche lo stesso programma è una lista.

Una lista può contenere una lista o un atomo. Un atomo è semplicemente tutto ciò che non è una lista (ad esempio stringhe, numeri, caratteri e nomi di funzioni).

Per generare esplicitamente una lista si può usare il comando list.

1
2
3
4
5
6
7
8
; Lista di numeri.
(list 1 2 3)

; Lista di stringhe.
(list "a" "b" "c")

; Lista di liste.
(list (list 1 2) (list "a") (list))

E via discorrendo fino ad avere contortissimi costrutti di liste piene di liste di liste. E’ importante notare che data una lista (a b c d … z) Scheme interpreta sempre il primo elemento di una lista come un comando. Quindi quando vogliamo indicare una lista pura dobbiamo sempre usare come primo elemento il comando list.

DEFINIZIONI

In Scheme tutte le definizioni, siano esse di variabili intere, stringhe, ma anche funzioni e procedure sono definite grazie al comando define.

1
2
3
4
5
; Variabile numerica.
(define pippo 3)

; Stringa.
(define pluto "Ciao")

E come abbiamo detto anche le funzioni possono essere definite in questo modo.

(define (somma a b) (+ a b))

Ecco che abbiamo definito una semplice funzione somma che potrà essere richiamata così:

(somma 3 4)

Una funzione è quindi definita secondo il formato:

(define (nomefunzione parametro1 parametro2 .... parametroN) (corpo della funzione))

RICAPITOLANDO:

  • Scheme è un dialetto del LISP.
  • Scheme è un linguaggio multiparadigma incentrato però sul paradima funzionale.
  • In Scheme ogni cosa racchiusa fra parentesi è un lista.
  • In Scheme ogni cosa che non è una lista si chiama Atomo (numeri, stringhe e altro).
  • In Scheme si utilizza esclusivamente la notazione prefissa anche per le operazioni aritmetiche.
  • In Scheme tutto si definisce con il comando define. Sia variabili che costanti che funzioni.
  • In Scheme il primo elemento di una lista è sempre visto come un comando.

 
Vi verrà lo sturbo.

Vi verrà lo sturbo.

Nel corso di Modelli e Complessità che sto seguendo è stato introdotto il Lisp (List Processor o meglio Lots of Infuriating & Silly Parenthesis) come esempio di linguaggio funzionale.

Come ho accennato in passato la programmazione funzionale è un tipo di paradigma che pone l’accento sulla definizione di funzioni poichè il flusso del programma assume la forma di valutazione di funzioni matematiche e in particolare delle funzioni ricorsive.

Senza addentrarci troppo in tecnicismi di informatica teorica passiamo subito ad affrontare la “pratica” con il linguaggio Lisp.

Continue reading »

© 2008-2012 SlashCode Suffusion theme by Sayontan Sinha