Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
LISP
Carlo Lepori
Scuola di Ingegneria del Canton Ticino
(STS)
Introduzione
In questa parte del corso “Linguaggi di programmazione” affronteremo al-
cune tematiche riguardanti la struttura e l’interpretazione dei programmi
per calcolatore, rifacendoci al testo: H. Abelson e G. Sussman, Structure
and Interpretation of Computer Programs, Mit Press. Altri temi, come
l’introduzione a linguaggi specifici (C++, C, ADA, ecc) e un’introduzione
alla teoria dei linguaggi formali (compilazione) sono sviluppati in altre parti
del corso.
Come nel testo citato, useremo il linguaggio Lisp, e questo per vari motivi:
1
2
Lisp>
3
CHAPTER 1. LA FUNZIONE COME ASTRAZIONE 4
ricorsività della definizione: un elemento di una lista può essere a sua volta
una lista, ecc.
Il valore di un atomo numerico è il numero stesso:
Lisp> 486
486
Lisp> pi
3.14 . . .
Lisp> a
error: unbound value . . .
Break 1>
Lisp> (* 5 99)
495
Come si vede, il Lisp usa una notazione algebrica di tipo a prefisso e non
quella tradizionale di tipo a infisso: questa scomodità è compensata da
una notevole uniformità sintattica. Le funzioni matematiche usate in questi
esempi, permettono anche un numero indeterminato di argomenti: grazie
alla notazione a prefisso non ci sono ambiguità:
Lisp> (* 25 4 12)
1200
Lisp> (+ (* 3 5) (- 10 6))
19
Lisp> dieci
10
Lisp> dieci
22
1.1.3 L’editor
Di solito le nuove funzioni non sono definite direttamente nell’interprete,
ma sono preparate in un editor e poi caricate nell’ambiente Lisp. Qualsiasi
editor va bene, ma dato il numero assai grande di parentesi che si possono
trovare in una definizione e la necessità di alternare il lavoro nell’editor e nel
Lisp, è conveniente usare un editor incorporato. Esso può venire invocato
con la funzione4
(ed "nome-del-file")
Gli argomenti di cond sono liste (dette clausole (clauses)) il cui primo ele-
mento è detto antecedente e il resto conseguenza. L’antecedente è un pre-
dicato: e cioè un’espressione il cui valore può essere vero o falso. In Lisp,
“falso” è il valore del simbolo nil, ogni altro valore viene considerato come
“vero”; talvolta si usa il simbolo t.6
La valutazione di un’espressione condizionale segue la seguente regola: viene
valutato il primo predicato; se esso è falso, viene valutato il secondo predicato
ecc. , finché si trova un predicato che è vero, cioè che dà un valore non-nil:
in questo caso si valutano le forme che compongono la conseguenza: il valore
dell’ultima è il valore di tutta l’espressione condizionale. Se nessun predicato
è vero, l’espressione condizionale ha il valore nil. Anche se più predicati
fossero veri, solo il primo di essi causa la valutazione della sua conseguenza:
l’ordine delle clausole è quindi importante!
Un altro modo di definire il valore assoluto:
Qui è stato usato il simbolo t che, essendo vero, fa scattare l’esecuzione della
forma corrispondente: è buona abitudine terminare la forma condizionale
con una clausola che inizia con t. Ancora un’altra possibilità:
(defun sum-of-squares (x y)
(+ (square x) (square y)))
Lisp> (sum-of-squares 3 4)
25
(defun f (a)
(sum-of-squares (+ a 1) (* a 2)))
Lisp> (f 5)
136
(f 5)
(sum-of-squares (+ a 1) (* a 2))
CHAPTER 1. LA FUNZIONE COME ASTRAZIONE 9
(sum-of-squares (+ 5 1) (* 5 2))
(+ (* 6 6) (* 10 10))
che si riduce a
(+ 36 100)
e quindi a
136
(f 5)
diventerebbe successivamente
(sum-of-squares (+ 5 1) (* 5 2))
(+ (* (+ 5 1) (+ 5 1)) (* (* 5 2) (* 5 2)))
riducendosi poi a
(+ (* 6 6) (* 10 10))
(+ 36 100)
136
n! = n · (n − 1)!
1! = 1
Questa definizione può essere programmata in Lisp direttamente:
contatore ← contatore + 1
CHAPTER 1. LA FUNZIONE COME ASTRAZIONE 11
Sebbene ambedue le definizioni abbiano forma ricorsiva (le funzioni sono de-
finite invocando sé stesse), i processi di calcolo creati dalle due funzioni sono
molto differenti: nel primo caso si ha un numero eventualmente elevato di
moltiplicazioni in sospeso: l’interprete ha bisogno di una quantità crescente
di memoria (proporzionale a n) per mantenere l’informazione necessaria: un
processo di questo tipo si dice linearmente ricorsivo (si veda la Tabella 1.1).
(factorial 6)
(* 6 (factorial 5))
(* 6 (* 5 (factorial 4)))
(* 6 (* 5 (* 4 (factorial 3))))
(* 6 (* 5 (* 4 (* 3 (factorial 2)))))
(* 6 (* 5 (* 4 (* 3 (* 2 (factorial 1))))))
(* 6 (* 5 (* 4 (* 3 (* 2 1)))))
(* 6 (* 5 (* 4 (* 3 2))))
(* 6 (* 5 (* 4 6)))
(* 6 (* 5 24))
(* 6 120)
720
Nel secondo caso in ogni momento la situazione è definita dal valore delle
tre variabili del programma: un processo di questo tipo è detto linearmente
iterativo (si veda la Tabella 1.2).
Non si confonda la forma della definizione della funzione, con il tipo di pro-
cesso generato! Quando una funzione ricorsiva genera un processo iterativo,
si parla di tail recursion.
Nei linguaggi tradizionali un processo iterativo viene descritto con strutture
sintattiche apposite (esistono anche in Lisp), per evitare lo spreco di memo-
ria inerente a un uso inutile di processi ricorsivi. Un buon Lisp dovrebbe
CHAPTER 1. LA FUNZIONE COME ASTRAZIONE 12
(factorial 6)
(fact-iter 1 1 6)
(fact-iter 1 2 6)
(fact-iter 2 3 6)
(fact-iter 6 4 6)
(fact-iter 24 5 6)
(fact-iter 120 6 6)
(fact-iter 720 7 6)
720
già fatti, per cui il processo impiega un tempo che cresce esponenzialmente
con n; lo spazio richiesto invece cresce linearmente. Questo comportamento
è tipico dei processi con ricorsione ad albero.
Un approccio iterativo a questo esempio, si basa sull’idea che partendo con
due numeri interi a e b inizializzati a 1 e 0 rispettivamente, con la trasfor-
mazione simultanea
a←a+b
b←a
dopo n trasformazioni b sará uguale a F (n).
Questo metodo è una iterazione lineare. Benché la differenza del tempo im-
piegato dai due metodi sia notevolissima, non si deve concludere che le ricor-
sioni ad albero siano inutili. In altri campi l’approccio ricorsivo rappresenta
un mezzo naturale e potente (Si ricordi che il funzionamento dell’interprete
è stato descritto in questi termini!).
(* 3 3 3)
(* y y y)
CHAPTER 1. LA FUNZIONE COME ASTRAZIONE 14
(defun sum-int (a b)
(if (> a b)
0
(+ a (sum-int (+ a 1) b))))
(defun sum-cubes (a b)
(if (> a b)
0
(+ (cube a) (sum-cubes (+ a 1) b))))
(defun pi-sum (a b)
(if (> a b)
0
(+ (/ 1 (* a (+ a 2))) (pi-sum (+ a 4) b))))
(defun sum-int (a b)
(sum #’+ a 1 b))
Per calcolare sum-cubes usiamo la funzione cube, che abbiamo già definito:
In generale è però scomodo dover definire col proprio nome funzioni che
non hanno un’utilità generale. Per definire pi-sum tramite sum sarebbe
7
In realtà si dovrebe usare la forma impropria function, ma per comodità di scrittura
i caratteri #’ hanno un effetto equivalente: (function sin) e #’sin sono la stessa cosa.
CHAPTER 1. LA FUNZIONE COME ASTRAZIONE 16
(defun pi-sum (a b)
(sum #’(lambda (n) (/ 1 (* n (+ n 2))))
a
4
b))
La funzione definita tramite lambda è una funzione come le altre (ma non
ha un nome), e può essere usato negli stessi contesti:
(lambda (x)
(/ (- (funcall f (+ x dx)) (funcall f x))
dx))
Si può andare oltre e definire la funzione deriv che prende come argomenti
una funzione f e un valore (piccolo) per dx e restituisce come valore la
derivata della funzione.
8
Il nome ha origine dalle teorie logiche sulla computabilità delle funzioni. Non bisogna
però lasciarsi spaventare: in Lisp serve solo allo scopo indicato!
9
Si tratta ancora di un calcolo numerico della derivata; vedremo in seguito che in Lisp
è possibile anche darne una definizione simbolica.
CHAPTER 1. LA FUNZIONE COME ASTRAZIONE 17
Possiamo ora usare la funzione cosı̀ definita per calcolare la derivata della
funzione cube nel punto 5 (il valore esatto è naturalmente 75):
Per mostrare le possibilità offerte dal calcolo della funzione derivata, voglia-
mo implementare l’algoritmo di Newton per gli zeri di una funzione differen-
ziabile. Se x0 è una approssimazione dello zero,
f (x0 )
x1 = x0 −
f 0 (x0 )
è una approssimazione migliore.
L’approssimazione successiva, a partire da un valore iniziale (qui chiamato
guess), viene espressa in Lisp come segue:
Finora abbiamo visto funzioni che hanno come valore numeri (o eventual-
mente altre funzioni), ma un linguaggio evoluto deve dare la possibilità di us-
are forme più complesse di valori, i cosiddetti dati strutturati. La definizione
di questo tipo di dati può essere difficoltosa: ripieghiamo sull’approccio
seguente:
Studieremo prima di tutto le strutture tipiche del Lisp, per poi affrontare il
tema in generale.
2.1 La lista
La struttura di dati fondamentale nel linguaggio Lisp è la lista1 : essa è pure
la forma sintattica fondamentale, realizzando la completa identità tra dati e
programmi, tipica del Lisp. Abbiamo già definito la lista a pag. 3, dicendo
che è un elenco di espressioni (simboliche) racchiuso da parentesi rotonde;
un’espressione a sua volta può essere un atomo o una lista.
1
Lisp significa appunto LISt Processing (e non Lots of Insane and Stupid Parentheses,
come affermano i detrattori)
18
CHAPTER 2. I DATI COME ASTRAZIONE 19
Il costruttore: cons
Data la definizione ricorsiva di lista, il costruttore avrà un approccio ricor-
sivo alla costruzione: esso aggiunge un elemento in testa alla lista data.
Supponiamo che il valore di lis sia (5 7 9):
Lisp> lis
(5 7 9)
Lisp> lis
(5 7 9)
Lisp> lis
(5 7 9)
Lisp> lis
(5 7 9)
Lisp> lis
(5 7 9)
Le espressioni simboliche
Una caratteristica del Lisp è la possibilità di lavorare a livello simbolico:
finora gli atomi visti erano numeri e i simboli avevano come valore un nu-
mero. Questa restrizione non è necessaria! Per poter operare con i simboli,
dobbiamo superare una difficoltà: nell’esempio:
Lisp> ’a
a
Per poter lavorare a livello simbolico sono indispensabili due predicati (che
insieme a cons, car e cdr formano le 5 funzioni base del Lisp): atom è vero
(t) se il suo unico argomento è un atomo (cioè non è una lista)4 ; eq ha due
argomenti (che dovrebbero essere atomi): esso è vero (t) se i due atomi sono
identici5 .
Sia per esempio da definire una funzione my-member che controlli se un
simbolo sia presente in una lista:
3
Si tratta di una stenografia per la forma impropria quote: (quote expr) e ’expr
sono la stessa cosa. Questi caratteri che influenzano il modo in cui l’interprete legge le
espressioni sono detti macro-characters (vedi anche la nota a pag. 15).
4
Attenzione: atom è uno dei pochi predicati che non terminano con la lettera p. È
anche disponibile il predicato listp che dà la risposta contraria.
5
Va usato solo per atomi simbolici: negli altri casi si usi equal e per i numeri =; sono
a disposizione anche predicati per casi particolari: null controlla se l’argomento è nil o
la lista vuota, zerop controlla se un numero è zero, ecc.
CHAPTER 2. I DATI COME ASTRAZIONE 22
Questo esempio illustra il metodo per lavorare con una lista: prima si studia
il first della lista e poi il rest con una chiamata ricorsiva. Qual è il valore
della funzione boh?
Due primitive molto utili, lavorando con liste, sono list e append. La
prima prende un numero variabile di espressioni come argomenti e crea una
lista che le contiene; la seconda prende un numero variabile di liste come
argomenti e ne fa una lista sola:
r r
@ (a . b)
@
R
@
a b
r r
HH
j
H
r nil a r rH
HHj
a b r nil
c
(a . nil) (a . (b . (c . nil)))
(a) (a b c)
r rXX
XXX
9
z
X
r rH r nil
HH
j
a r c
nil
b
((a b) c)
Lisp> lista
(mele pere noci)
lista1 - r r - r nil
(a b)
? ?
a b
6 6
?
lista2 - r r - r nil
(c d)
? ?
c d
26