Appunti a.a. 2005/2006
Il presente file è destinato
agli studenti/studentesse del corso di Informatica
Applicata per la Didattica, Facoltà di
Scienze della Formazione Primaria dell'Università di Udine.
In particolare, questi appunti sono rivolti
a quella categoria di studenti che non ha la
possibilità di frequentare il corso e ritiene
di avere lacune nella preparazione matematica.
Per questo fine, l'esposizione è
sviluppata in modo dettagliato e progressivo,
con corredo di numerosi esempi e indulgendo talora in dettagli e/o spiegazioni
probabilmente scontati. Il file è “self-contained”,
ossia non presuppone, per essere compreso,
che conoscenze di matematica elementare.
L'obbiettivo è arrivare a scrivere
programmi nel linguaggio di programmazione Scheme che consentano
di risolvere semplici problemi nei campi dell'aritmetica e dell'algebra.
Ai lettori con esigenze di più ampio spettro e desiderio
di approfondimento, va detto che il materiale
introdotto non è assolutamente esaustivo,
limitandosi ad essere funzionale ai problemi che vengono trattati.
Inoltre, in quanto non di immediato impatto
da un punto di vista “pratico”, vengono tralasciate
tutte le spiegazioni di natura teorica,
rimandando il lettore interessato ai testi specifici sull'argomento.
Di tanto in tanto compariranno
brevi commenti sull'utilizzo dell'applicazione
DrScheme.
Non tutto il programma del corso è coperto,
pertanto è necessaria
una integrazione con altro materiale (testi segnalati, appunti del corso).
Gli studenti possono chiedere chiarimenti sugli
argomenti qua trattati, e sono invitati a proporre suggerimenti/modifiche
e correggere gli errori che inevitabilmente farò:
sarò lieto di soddisfare, nei limiti
del possibile, le richieste che riceverò
(all'indirizzo alessi@dimi.uniud.it).
ATTENZIONE: su alcuni monitor non appare una chiara
discriminazione degli spazi fra caratteri.
Quanto segue è un piccolo test per verificare se riuscite
a discernere correttamente l'inserzione di spazi fra caratteri.
Osservate la riga sottostante:
(<r 23)
(< r23)
(<r 2 3)
(<r23)
(< r 23)
Si danno due casi:
- sul vostro monitor le 5 espressioni sopra appaiono
in modo graficamente distinto, nel qual caso non ci saranno
ambiguità nella lettura delle espressioni in Scheme;
- sul vostro monitor alcune delle 5 espressioni appaiono
quasi identiche. In questo caso dovete prestare,
soprattutto agli inizi, una certa
attenzione alle piccole differenze di spaziatura presenti
nella scrittura dei programmi in Scheme.
In seguito,
una volta presa dimestichezza con la scrittura del codice,
non incontrerete più alcuna difficoltà nel leggere nel modo
corretto le espressioni.
Sulle espressioni aritmetiche in Scheme
Le operazioni aritmetiche in Scheme che verranno prevalentemente impiegate
sono le seguenti:
- + (somma)
- - (differenza)
- * (prodotto)
- quotient (quoto o quoziente
della divisione intera)
- remainder (resto della divisione intera)
Queste operazioni, quando gli argomenti sono numeri interi,
restituiscono come risultato ancora numeri interi.
Altre operazioni utilizzate (il cui risultato può non essere intero)
sono:
-
/ (l'usuale divisione)
- sqrt (estrazione della radica quadrata)
Riguardo alla doppia possibilità di usare
quotient o / per l'operazione di divisione, si tenga
presente la distinzione fra la divisione intera (spesso indicata
con il simbolo “:”) e la divisione
decimale (ad esempio: 13:3 = 4; 13/3 = 4.3333...).
La prima richiede due argomenti (dividendo e divisore) interi, e produce
come risultato sempre un numero intero, mentre la seconda
può produrre un risultato nel campo delle frazioni (o
dei numeri reali, se dividendo e/o divisore sono numeri reali).
Se un programma richiederà di utilizzare
la divisione intera, si sceglierà la procedura quotient.
(anche se, in molti casi, l'uso di / resta
possibile). Programmi che invece richiedono l'uso di frazioni e/o
numeri reali, faranno viceversa ricorso
a / per il calcolo della divisione decimale.
Le espressioni aritmetiche, che fanno uso
di tutte le operazioni sopra menzionate vengono
scritte in Scheme secondo la sintassi
(<operazione> <argomento1> <argomento2>)
con l'eccezione della radice quadrata, che vuole un unico argomento.
Si presti attenzione alla presenza delle parentesi tonde,
e al fatto che l'operazione va scritta dopo
l'apertura della parentesi, prima dei suoi argomenti.
Si rammenta di lasciare almeno
uno spazio a separare operazioni e argomenti, mentre potete
scegliere a piacere se lasciare o meno spazi fra parentesi
tonde aperte e operazioni, o fra parentesi tonde chiuse e argomenti.
Ecco qualche esempio:
la somma 3 + 4, la differenza 10 - 8, il prodotto 5 * 4 si scrivono in Scheme
rispettivamente
(+ 3 4)
(- 10 8) (* 5 4)
Con riferimento alla spaziatura,
altre possibilità di scrittura di 3 + 4 sono ad esmpio queste:
( + 3 4)
(+ 3 4 )
ma non ad esempio
( +3 4 )
( + 34 )
La radice quadrata di 3 si scrive in Scheme
(sqrt 3)
Il quoziente della divisione intera e il resto della
divisione intera fra 17 e 2 (risultati: 8 e 1 rispettivamente)
si scrivono in Scheme come segue:
(quotient 17 2) (remainder 17 2)
I numeri negativi
Si scrivono in Scheme
facendo precedere il valore assoluto del numero dal segno -,
SENZA spaziatura. Alternativamente, possono venire scritti
facendo precedere il valore assoluto del numero dal segno -,
CON spaziatura, il tutto racchiuso da parentesi tonde.
Ad esempio la somma (-3) + (-17) può essere scritta
nei seguenti due modi:
(+ -3 -17) oppure (+ (- 3) (- 17))
Se una quantità algebrica è preceduta da un segno
meno, si usa la seconda opzione.
Ad esempio, l'espressione a + (-b) viene scritta
(+ a (- b))
e non (+ a -b).
Espressioni più complesse possono essere scritte annidando
le traduzioni delle sottoepressioni
nel rispetto della sintassi stabilita sopra,
e ricordandosi di aggiungere, se occorre, opportune parentesi tonde.
Nel passare dalla espressione scritta nell'usuale formato
matematico alla sua traduzione in Scheme, è utile
procedere passo passo, traducendo prima le sottoespressioni più semplici
e via via procedendo verso quelle più complesse.
Nella fase intermedia del processo di traduzione,
le espressioni già tradotte in Scheme sono riconoscibili
per avere l'operazione a sinistra. L'esempio successivo al seguente
chiarirà quanto affermato.
Ecco qualche esempio: l'espressione
3 + 4*5, viene così riscritta
(+ 3 (* 4 5))
Traduciamo ora l'espressione (4 + 2) * (2/3 + (-5/6)*7).
Procediamo
in più passi, a partire dalle sottoestressioni più
semplici. In quanto segue sotto, la riga iniziale
contiene l'espressione di partenza;
la riga finale contiene l'espressione interamente
tradotta in Scheme; le righe intermedie contengono espressioni
spurie, parzialmente tradotte in Scheme. Si presti la massima
attenzione alla priorità delle operazioni, decisiva
per scrivere correttamente la traduzione in Scheme dell'espressione
aritmetica
(4 + 2) * (2/3 + (-5/6)*7) →
(+ 4 2) * ((/ 2 3) + (/ (- 5) 6)*7) →
(+ 4 2) * ((/ 2 3) + (* (/ (- 5) 6) 7)) →
(+ 4 2) * (+ (/ 2 3) (* (/ (- 5) 6) 7)) →
(* (+ 4 2) (+ (/ 2 3) (* (/ (- 5) 6) 7)))
Un'espressione già munita di parentesi quadre come
[(5 + 4)/(6 * 7) - 8] * 17 viene comunque riscritta usando
solo parentesi tonde, come segue:
(* (- (/ (+ 5 4) (* 6 7)) 8) 17)
Una utile semplificazione che si può impiegare
è l'utilizzo di una unica operazione di somma
o prodotto quando l'espressione da tradurre coinvolge una sola
di queste operazione.
Ad esempio, l'espressioni aritmetica
3 + 4 + 6 + 10, anziché venire tradotta (correttamente,
comunque) come
(+ 3 (+ 4 (+ 6 10)))
può, più brevemente, essere scritta così:
(+ 3 4 6 10)
Una analoga possibilità esiste con i prodotti:
(8 * 7 * 9) può essere scritto
(* 8 7 9)
Espressioni algebriche
si scrivono in modo del
tutto analogo a quelle aritmetiche (attenzione, però: una espressione
algebrica avrà senso solo se inserita in certi contesti,
si veda il seguito). Consideriamo ad esempio la formula
che dà una delle due soluzioni della equazione
di secondo grado ax2 + bx + c = 0.
La formula che vogliamo scrivere in Scheme è la seguente:
[-b + √(b2 - 4ac)]/(2a)
Eccone le traduzione in Scheme:
(/ (+ (- b) (sqrt (- (* b b) (* 4 a c)))) (* 2 a))
Sulle definizioni in Scheme
Le definizioni in Scheme vengono impiegate per dare nomi
ad espressioni: il nome assegnato ad una data espressione
nel processo di definizione è a tutti gli effetti
utilizzabile al posto della espressione.
Le definizioni, nell'ambiente DrScheme, vengono in genere
scritte nella finestra delle definizioni (quella in alto).
Per renderle operative, occorrerà poi cliccare sul
tasto “Run” o “Execute” (a seconda
delle versioni scaricate di DrScheme).
Le espressioni a cui si decide di dare un nome possono
essere di vario tipo: da oggetti semplici come i numeri
a procedure/programmi etc.
Le definizioni sono introdotte dalla parola riservata
“define” ed hanno la seguente sintassi:
(define <nome> <espressione>)
Il nome può essere scelto a piacimento e consiste
in una successione di caratteri alfanumerici (ad esempio:
x, y, z, a, b, c, oppure a1, a-1, a_1, a2, x_1, z3 etc, oppure
lato1, lato_2, raggio, altezza, etc etc: si veda il seguito).
In genere è buona norma usare nomi che siano espressivi
del significato della espressioni a cui sono associati.
Esempio 1: Un euro vale 1936.27 lire. Al fine
di utilizzare tale fattore di conversione senza dovere
ricordare a memoria il numero, possiamo introdurre una definizione
per tale costante numerica. Dobbiamo solo scegliere un nome
appropriato, come ad esempio: fattore_conversione.
La definizione sarà pertanto:
(define fattore_conversione 1936.27)
Una volta scritta questa definizione, il nome
“fattore_conversione” resterà associato
al numero 1936.27 e potrà essere utilizzato in sua vece.
Ad esempio, potremo creare l'espressione aritmetica
(* 2 fattore_conversione) che, una volta calcolata, darà
come risultato 3872.54.
Esempio 2: una costante della massima importanza in geometria
è π, ossia Pi greco,
il cui valore approssimato è 3.14.
Un valore molto più preciso si può ottenere
utilizzando la funzione predefinita acos (arcoseno),
calcolando l'arcoseno di -1: ossia, π = acos(-1).
Tradotto in Scheme, acos(-1) viene scritto (acos -1)
oppure (acos (- 1)).
A questo punto possiamo senz'altro passare alla definizione
di π, stabilendo per esso il nome “pigreco” (e non
“pi greco”: gli spazi
fra le lettere del nome non sono ammessi; viceversa
la scelta del nome “pi_greco” è
altrettanto buona).
(define pigreco (acos -1))
Come nell'esempio precedente, il nome “pigreco”
è ora associato al valore di π.
Ad esempio, ricordando che πR2 è la formula
per il calcolo dell'area di un cerchio (dove R è
il raggio del cerchio), l'espressione
calcolerà l'area di un cerchio di raggio 3.
Naturalmente più nomi (precedentemente introdotti
in qualche definizione) possono comparire
all'interno di una espressione. Ad esempio
(- fattore_conversione pigreco)
calcolerà la differenza (probabilmente di nessuna utilità!)
fra 1936.27 e π.
Primi programmi in Scheme
Scrivere programmi in Scheme è relativamente semplice.
Da un punto di vista concettuale si richiede poco più che
la comprensione del processo definitorio di una usuale
funzione matematica.
Partiamo da un esempio: si consideri la funzione
f(x) = 3x - 4
definita sui numeri interi.
In questa definizione distinguiamo:
- il nome dato alla funzione, in questo caso “f”;
- i parametri coinvolti (ossia i nomi che appaiono all'interno
della parentesi che segue il nome della funzione), in questo caso
uno solo, “x”;
- il corpo della definizione, ossia ciò che sta
a destra del simbolo dell'uguaglianza, e che costituisce
l'espressione che verrà sinteticamente descritta
da f(x). Nel nostro caso 3x - 4.
La definizione di f sottintende il seguente uso:
a f viene passato, di volta in volta, un argomento differente,
che verrà sostituito a x, dopo di che si svolgeranno le operazioni
prescritte nel secondo membro dell'uguaglianza.
Così, ad esempio, calcolare f applicata all'argomento 5,
in formule f(5), significa sostituire al posto della x, il valore
5 nella espressione 3x - 4, ottenendo 3*5 - 4, quindi 11 come
risultato finale. Similmente f(7) darà 17 come risultato
e così via.
Un secondo esempio, che coinvolge un numero maggiore
di parametri, è la funzione che esprime il volume
di un cilindro in funzione del raggio e dell'altezza
del cilindro stesso (rammentiamo la formula:
volume del cilindro = π*raggio2*altezza).
Se usiamo il nome “rag”
per il raggio del cilindro e il nome
“alt” per l'altezza, e chiamiamo volume_cilindro
la funzione, perverremo a questa definizione
volume_cilindro(rag, alt) = π*rag2*alt
Naturalmente c'è un ampio margine di scelta per quanto
riguarda i nomi della funzione e dei parametri: saremmo
pervenuti ad una definizione del tutto equivalente a quella data
scrivendo ad esempio:
g(a, b) = π*a2*b
avendo convenuto di chiamare “g” la funzione
volume, e “a”, “b”, rispettivamente
il raggio e l'altezza del cilindro. I nomi scelti nella
prima definizione (volume_cilindro, rag, alt)
sono più scomodi, in quanto più lunghi,
ma anche più espressivi del ruolo giocato dalle
quantità coinvolte, e più in linea con le
esigenze della programmazione, dove spesso, stante la lunghezza
del codice scritto, è utile avere nomi che
richiamano il ruolo svolto dalle quantità associate
ai nomi stessi.
Nel caso della funzione volume_cilindro, il processo di sostituzione
degli argomenti ai parametri avviene in modo ordinato: se noi
applichiamo la funzione ai valori 4 e 6 nell'ordine, ossia
calcoliamo volume_cilindro(4,6), il numero 4 verrà sostituito
a rag e il numero 6 a alt, ottenendo come risultato approssimato
301.59. Calcolare volume_cilindro(6,4) significa viceversa
sostiuire 6 al posto di rag, 4 al posto di alt, ottenendo
un risultato differente, 452.38 circa.
La scrittura di un programma in Scheme richiede un processo
molto simile a quello visto sopra messo in atto per definire
le funzioni: i passi sono essenzialmente tre, di cui i primi due
semplici, l'ultimo a volte complesso:
- stabilire il nome da assegnare al programma;
- stabilire quali sono i parametri coinvolti e quali nomi
assegnare ad essi;
- scrivere il corpo della procedura, che
consiste nell'espressione che si dovrà valutare
per pervenire al risultato desiderato
Di fatto il secondo e il terzo passo sono associati, e
concorrono alla definizione della procedura (che quindi conterrà
la lista dei parametri e il corpo, secondo e terzo punto sopra).
Il punto 1 sopra viene assolto dal meccanismo di definizione
che abbiamo visto: per definire un programma inizieremo con
(define <nome_scelto> <procedura>)
Supponiamo, per fissare le idee, di volere scrivere un programma
che calcola l'area di un rettangolo, a partire dalla conoscenza
dei due lati, che chiameremo lato1 e lato2. Sembra ragionevole chiamare questo
programma area_rettangolo. L'inizio della definizione del nostro
programma sarà in questo caso
(define area_rettangolo .......)
Ora occorre riempire lo spazio occupato dai puntini con la
scrittura della procedura opportuna.
Come detto, la procedura contiene, nell'ordine, la lista
dei parametri impiegati e il corpo, ossia l'espressione che
permette di pervenire al risultato.
La sintassi per scrivere una procedura impiega
una parola riservata, “lambda”; a seguire,
la lista, fra parentesi tonde, dei parametri (si ricordi:
i parametri vanno spaziati e NON è ammesso separarli
mediante virgole!) e successivamente il corpo della procedura.
Ecco la sintassi:
(lambda (<parametro_1> ... <parametro_n>)
<corpo della
procedura> )
Un commento sul modo di distribuire la scrittura.
È diffuso l'uso di scrivere
sulla stessa riga, “lambda” e la lista dei parametri,
mentre il corpo della procedura (che molto spesso, come si vedrà,
consiste di una lunga espressione racchiusa fra parentesi tonde)
viene scritto andando a capo (e occupando le righe del caso).
Ci atteniamo a questo sistema di scrittura che risulta piuttosto
comodo per una agevole lettura di programmi/procedure.
Tornando al nostro programma area_rettangolo, la procedura la scriveremo
fissando i parametri (possiamo chiamarli come sopra: lato1 e lato2)
e scrivendo successivamente il corpo. Come detto, il corpo deve
racchiudere l'espressione che consente di pervenire al risultato.
Nel caso del nostro esempio, l'area del rettangolo si calcola
secondo la notissima formula (scritta nell'usuale linguaggio
matematico, scegliendo i nomi come sopra):
area_rettangolo(lato1, lato2) = lato1*lato2
Il secondo membro dell'uguaglianza è la formula che
ci interessa e che traduciamo immediatamente in Scheme:
(* lato1 lato2)
Quanto scritto sopra costituisce il corpo della procedura.
Ecco pertanto la procedura completa:
(lambda (lato1 lato2)
(* lato1 lato2) )
(Si osservino le parentesi: fra gli errori più comuni
c'è
un improprio uso di esse!). Ed ecco ora la definizione del
programma completo:
(define area_rettangolo
(lambda (lato1 lato2)
(* lato1 lato2) ) )
Questo programma si presta ad essere applicato a qualunque
coppia di argomenti numerici. Per testare il programma
nell'ambiente DrScheme, scriviamo ad esempio nella finestra delle interazioni
(Interaction Window: quella in basso)
(area_rettangolo 6 4)
Dopo
avere premuto il tasto INVIO, comparirà il risultato, 24.
Similmente scrivendo nella Interaction Window
(area_rettangolo 5 9)
e premendo INVIO, comparirà 45, e così via.
Esercizio 2.1:
Scrivere un programma che calcola l'area del
quadrato a partire dal lato L.
Sol. Si può procede come nel caso precedente. Diamo per prima
cosa un nome al programma, scegliamo in questo caso il nome
inglese “square” (quadrato).
Il parametro coinvolto è uno, la misura
del lato, che possiamo indicare con L come nel testo dell'esercizio.
Infine, il corpo della procedura deve contenere la formula
che permette di calcoloare l'area del quadrato, ossia
(* L L). Ecco il programma completo:
(define square
(lambda (L)
(* L L) ) )
Esercizio 2.2:
Scrivere un programma che calcola il cubo di un numero.
Sol.
Pochi commenti da fare rispetto all'esercizio precedente.
Potremmo chiamare “cubo” il programma,
“numero” il nome assegnato al parametro.
La formula da inserire nel corpo della procedura è
in questo caso (* numero numero numero).
Ricordiamo ancora che i nomi dati al programma ed ai parametri
sono scelti a piacere.
Ecco il programma completo:
(define cubo
(lambda (numero)
(* numero numero numero) ) )
Esercizio 2.3:
Scrivere un programma che calcola il perimetro
di un triangolo, a partire dalla misura dei lati.
Sol. Sinteticamente:
- nome dato al programma: perimetro_triangolo
- nomi assegnati ai parametri: lato1, lato2, lato3
- corpo della procedura: (+ lato1 lato2 lato3)
Programma completo:
(define perimetro_triangolo
(lambda (lato1 lato2 lato3)
(+ lato1 lato2 lato3) ) )
Prima di proseguire vediamo un esempio di errata
stesura del programma precedente. Provate, prima
di leggere i commenti, ad individuare gli errori fatti.
(define perimetro_triangolo)
(lambda (lato1, lato2, lato3)
(+ a lato2 lato3) ) )
Gli errori sono i seguenti: si chiude impropriamente una
parentesi tonda dopo il nome dato al programma (prima riga),
nella parentesi dedicata ai parametri, questi sono separati
dalle virgole (seconda riga), infine, nel corpo della procedura,
compare un parametro “a” che non è stato
precedentemente introdotto.
Esercizio 2.4:
Scrivere un programma che calcola l'area di
un triangolo, conoscendo base e altezza.
Sol.
- nome (possibile) dato al programma: area_triangolo
- nomi assegnati ai parametri: base, altezza
- corpo della procedura: (/ (* base altezza) 2)
Programma completo:
(define area_triangolo
(lambda (base altezza)
(/ (* base altezza) 2) ) )
Esercizio 2.5:
Scrivere un programma che calcola l'area di
un trapezio, conoscendo la base maggiore, la base minore e l'altezza
(la formula è: (somma delle basi * altezza) diviso 2)
Sol.
- nome dato al programma: area_trapezio
- nomi assegnati ai parametri: BASE, base, altezza
- corpo della procedura: (/ (* (+ BASE base) altezza) 2)
Programma completo:
(define area_trapezio
(lambda (BASE base altezza)
(/ (* (+ BASE base) altezza) 2) ) )
Esercizio 2.6:
Scrivere un programma che calcola l'area del cerchio
a partire dal raggio R (la formula è area = πR2).
Sol. Questo programma presuppone la definizione di una
costante che racchiuda una approssimazione del valore di π.
Questo passo è già stato spiegato in una
sezione precedente, ma lo riportiamo
comunque sotto per comodità del lettore, con una piccola
variazione: sceglieremo, per l'approssimazione di π,
il nome “pi_greco” (invece di
“pigreco”).
- nome dato al programma: area_cerchio;
- nomi assegnati ai parametri: R
- corpo della procedura (* pi_greco R R)
Ecco il programma, preceduto dalla definizione relativa a π:
(define pi_greco (acos -1))
(define area_cerchio
(lambda (R)
(* pi_greco R R) ) )
Esercizio 2.7
Scrivere un programma che prende come argomento
un prezzo in euro e lo converte in lire.
Sol. Procediamo come nel precedente esercizio.
Avevamo in altra sezione introdotto il fattore di conversione
euro-lire, attraverso la definizione
(define fattore_conversione 1936.27)
Utilizzeremo questa definizione nella stesura del programma.
- nome dato al programma: converti_euro->lire
- nome assegnato al parametro: prezzo
- corpo della procedura: (* prezzo fattore_conversione)
Ecco il programma completo:
(define converti_euro->lire
(lambda (prezzo)
(* prezzo fattore_conversione) ) )
Esercizio 2.8: Nel paese X i redditi vengono
tutti tassati del 8%. Scrivere un programma che, dati i redditi
lordi, calcola i redditi al netto della tassa.
Sol.
Chiamiamo “r” il reddito lordo. Per calcolare il reddito
al netto della tassa, dovremo sottrarre a r l'8% di r stesso,
secondo la formula:
reddito_netto = r - (8/100)r
La traduzione in Scheme della espressione sopra costituirà il corpo
della procedura.
- nome assegnato al programma: calcola_tassa
- nomi assegnati ai parametri: r
- corpo della procedura: (- r (* (/ 8 100) r) )
Ecco il programma completo
(define calcola_tassa
(lambda (r)
(- r (* (/ 8 100) r )) ) )
Verso programmi più complessi
Programmi che contengono programmi
È possibile e sovente utile richiamare programmi
già scritti all'interno dei programmi che stiamo
scrivendo. Questa possibilità è illustrata
nel prossimo esercizio.
Esercizio 3.1:
Si hanno un triangolo di base b1 e altezza h1,
un trapezio di base maggiore B2, base minore b2 e altezza h2,
e infine un cerchio di raggio R. Scrivere un programma
che calcola l'area complessiva occupata dalle tre figure.
Sol. Se, nella finestra delle definizioni di DrScheme,
abbiamo già a disposizione
i programmi precedenti “area_triangolo”
“area_trapezio” e “area_cerchio”,
(unitamente alla definizione di π), possiamo
abbreviare la scrittura del nostro
programma, utilizzando al suo interno i suddetti programmi,
e facendo quindi uno sforzo di elaborazione minore,
in quanto non dovremo calcolare la formula dettagliata che somma
le varie aree (vedi sotto).
Infatti l'area del triangolo la potremo calcolare
scrivendo (area_triangolo b1 h1), quella del trapezio
scrivendo (area_trapezio B2 b2 h2) e l'area del cerchio
scrivendo (area_cerchio R). Si dovrà poi sommare
queste aree. Procediamo:
- nome dato al programma: calcola_somma_aree
- nomi assegnati ai parametri: b1, h1, B2, b2, h2, R
- corpo della procedura:
(+ (area_triangolo b1 h1)
(area_trapezio B2 b2 h2) (area_cerchio R))
Il corpo dalla procedura, costituito da una espressione
abbastanza lunga, verrà spezzato su tre righe,
in modo da rendere agevole la lettura del programma.
Ecco il programma completo:
(define calcola_somma_aree
(lambda (b1 h1 B2 b2 h2 R)
(+ (area_triangolo b1 h1)
(area_trapezio B2 b2 h2)
(area_cerchio R) )))
Una breve parentesi:
come dovrebbe essere ormai chiaro, se nella
Interaction Window scriviamo l'espressione
(calcola_somma_aree 6 4 8 5 3 9)
e premiamo INVIO, otterremo l'area complessiva ottenuta
sommando le aree di un triangolo di base 6 e altezza 4,
di un trapezio di base maggiore 8, base minore 5 e altezza 3,
e di un cerchio di raggio 9.
Torniamo al discorso principale sui programmi contenenti
altri programmi.
Se, a differnza di quanto visto sopra, non avessimo a disposizione o non
volessimo usare i 3 sottoprogrammi,
saremmo costretti a scrivere
un programma più lungo. A parte questo difetto, che
può essere in certi casi marginale, è la comprensione
del corpo della procedura che viene inficiata (quasi sempre).
Ecco il programama riscritto non facendo uso dei
3 sottoprogrammi:
(define calcola_somma_aree2
(lambda (b1 h1 B2 b2 h2 R)
(+ (/ (* b1 h1) 2) (/ (* (+ B2 b2) h) 2) (* (acos -1) R R))))
Potremmo forse migliorare la comprensione del corpo della
procedura spezzando su più righe la sua scrittura,
ma in ogni caso si otterrebbe una espressione più oscura
rispetto a quella del programma precedente.
Test ed espressioni condizionali
Oltre ai numeri (interi, reali, complessi), un
altro tipo di dato è costituito dai valori booleani,
true (vero) e false (falso), che in Scheme vengono talora
scritti rispettivamente come #t e #f. Chiameremo test una qualunque
espressione il cui processo di valutazione condurrà
ad un valore booleano. Semplificando (e banalizzando) il discorso,
i test sono espressioni che presuppongono una domanda evidenziata
dalla presenza di un operatore di confronto.
Iniziamo a presentare i test fra numeri. Gli operatori di
confronto che useremo sono i seguenti:
- = (uguale)
- < (minore)
- > (maggiore)
- <= (minore uguale)
- >= (maggiore uguale)
I test vengono scritti, come d'uso per le espressioni in Scheme, ponendo
in testa alla espressione l'operatore di confronto.
Ad esempio, i test x = 2, (1+4) >= (2+8), 4 < 30/7, vengono
scritti in Scheme rispettivamente come segue:
(= x 2) (>= (+ 1 4) (+ 2 8))
(< 4 (/ 30 7))
I test precedenti sono destinati a dare come risultato true oppure false.
Sul primo test non possiamo dire nulla (è un test il cui esito
dipende dal valore del parametro x: test di questo tipo compariranno
molto spesso nel corpo di procedure). Del secondo e terzo test
possiamo invece scrivere il risultato finale: false per il secondo
test, true per il terzo. A questi risultati si perviene anche scrivendo
i due test nella Interaction Window di DrScheme e premendo INVIO.
Scrivere invece il primo test, isolato, nella
Interaction Window (e non all'interno di una procedura)
produrrà probabilmente un messaggio di errore simile al seguente:
“ ...x unundefined identifier...”, indicativo del fatto
che il nome x non è stato precedentemente introdotto
e quindi è impossibile darne una valutazione.
I test possono essere fra loro combinati attraverso i
connettivi logici:
- “and” (congiunzione)
- “or” (disgiunzione)
- “not” (negazione)
La congiunzione “and” può essere applicata ad un numero
arbitrario di test, secondo la sintassi
(and <test-1> <test-2> ...
<test-n>)
e produce true come risultato se e solo se TUTTI i test a cui viene
applicata danno, ciascuno, true come risultato, altrimenti
il risultato è false. Ad esempio, il test
(and (< 1 (+ -1 3)) (= 5 (sqrt
25)) (>= 100 (* 9 9)) )
dà risultato true,
poiché tutti e 3 i test interni sono veri. Viceversa, il test
(and (< 0 2) (= 7 (/ 72 8)) )
dà risultato false, poiché il secondo test interno
dà false come risposta.
La disgiunzione “or” è anch'essa applicabile
ad un numero arbitratio di test, secondo la sintassi
(or <test-1> <test-2> ...
<test-n>)
e produce come risultato true se e solo se ALMENO un test a cui viene
applicata dà true come risultato, altrimenti il risutlato
è false. Ad esempio, il test
(or (> 1 (+ -1 3))
(= 5 (sqrt 26)) (>= 100 (* 9 9)) )
dà true come risultato, poiché
l'ultimo test ha come risultato
true, e questo basta per concludere che true è il risultato
del test complessivo. Viceversa, il test
(and (>= 0 2) (= 7 (/ 72 8)) )
(< 20 (* 4 4) ) (< 0 (- 1 1)) )
dà risultato false, poiché tutti i test interni danno
false come risultato.
La negazione “not” si applica ad un unico argomento, secondo
la sintassi
(not <test>)
e produce come risultato il valore opposto al valore di verità del
test a cui è applicata. Ad esempio, i seguenti test
(not (< 0 4) )
(not (and (< 5 3) (> 8 (+ 1 7)) ) )
producono rispettivamente false e true come risposta. Nel primo
test, dopo il “not” segue un test vero, per cui la
risposta complessiva sarà false. Nel secondo test,
la congiunzione interna è resa falsa dalla presenza del test falso
(> 8 (+ 1 7)); pertanto il valore complessivo
dell'espressione è true.
Esercizio 3.2:
Scrivere un programma “pari?” che seleziona i numeri pari:
risponde true se un intero n è
pari, false altrimenti.
Sol. Da pari? ci aspettiamo il seguente comportamento:
(pari? 14) → true
(pari? 22) → true
(pari? 19) → false
Il programma pari? si basa su un test che farà uso
del parametro n. Il test deputato a stabilire
se n è pari o dispari è
(= 0 (remainder n 2) )
(un numero è pari se e solo se dà resto
0 quando è diviso per 2).
Ecco i dati relativi al nostro programma:
- nome assegnato al programma: pari?
- nomi assegnati ai parametri: n
- corpo della procedura: (= 0 (remainder n 2) )
Pertanto il programma completo ha questo codice:
(define pari?
(lambda (n)
(= 0 (remainder n 2) ) ))
Esercizio 3.3:
Scrivere un programma p_divide_n? che, presi come argomenti
due interi n e p, stabilisce se n è divisibile per p.
Sol. Questo esercizio è molto simile al precedente.
Passiamo direttamente alla soluzione senza commenti:
- nome assegnato al programma: pari?
- nomi assegnati ai parametri: n, p
- corpo della procedura: (= 0 (remainder n p) )
Pertanto il programma completo ha questo codice:
(define p_divide_n?
(lambda (n p)
(= 0 (remainder n p) ) ))
Ecco qualche esempio di sua applicazione:
(p_divide_n? 10 5) → true
(p_divide_n? 11 4) → false
(p_divide_n? 42 7) → true
Esercizio 3.4
Scrivere un programma PROG che seleziona tutti i numeri divisibili
per 7 compresi nell'intervallo [0,50] (estremi compresi).
Sol. Esempi di applicazione:
(PROG 14) → true
(PROG 23) → false
(PROG 70) → false
Il test su cui PROG si basa è composto da 3 sottotest
a cui si applica la congiunzione. Infatti un numero n supera il test
del programma se e solo se
- n è minore o uguale a 50 e inoltre
- n è maggiore o uguale a 0 e inoltre
- n è divisibile per 7
Le traduzioni in Scheme dei tre sottotest sono rispettivamente:
(<= n 50)
(>= n 0)
(= 0 (remainder n 7) )
Pertanto il test utilizzato dal programma è
(and (<= n 50) (>= n 0)
(= 0 (remainder n 7)) )
Ecco quindi il programma completo:
(define PR0G
(lambda (n)
(and (<= n 50)
(>= n 0)
(= 0 (remainder n 7)) ) ) )
Esercizio 3.5:
Sia A un insieme di numeri interi così definito:
un numero n appartiene ad A se è pari e compreso nell'intervallo
[40,100] (estremi compresi) oppure se è maggiore di 1000.
Scrivere un programma che risponde true se n appartiene ad A,
altrimenti risponde false.
Sol.
A l'insieme (infinito) dei numeri selezionati è:
A = {40, 42, 44, .... ,96, 98, 100, 1001, 1002, 1003, 1004 ....}
Dal programma cercato, che chiameremo “Seleziona_A”,
ci aspettiamo, ad esempio, il comportamento seguente:
(seleziona_A 52) → true
(seleziona_A 104) → false
(seleziona_A 3000) → true
Il programma Seleziona_A si basa su un test che farà uso
del parametro n e che stabilirà, rispondendo true o false,
se n appartiene o meno ad A.
Per questo fine notiamo che A è definito attraverso una
disgiunzione (numeri pari dell'intervallo [40,100], oppure
numeri maggiori di 1000). Per quanto riguarda i numeri n maggiori
di 1000, questi vengono selezionati dal test
(> n 1000)
Chiamiamo, per il momento, T1 il test che seleziona i numeri
pari dell'intervallo [40,100].
Possiamo pertanto scrivere una prima approssimazione del
test finale che verrà usato dal programma:
(or T1 (> n 1000) )
Dobbiamo ora scrivere esplicitamente T1.
T1 deve selezionare gli interi n tali che:
- n è minore o uguale a 100 e inoltre
- n è maggiore o uguale a 40 e inoltre
- n è pari
T1 è quindi composto
da una congiunzione dei tre sottotest scritti sopra.
Le traduzioni in Scheme dei tre sottotest sono
rispettivamente:
- (<= n 100)
- (>= n 40)
- (= 0 (remainder n 2))
Possiamo a questo punto
scrivere T1:
(and (<= n 100) (>= n 40) (= 0 (remainder n 2)) )
Siamo ora in grado di scrivere il test completo del nostro programma:
(or (and (<= n 100) (>= n 40) (= 0 (remainder n 2)) ) (> n 1000) )
La riga scritta sopra (che nella stesura del programma spezzeremo
per comodità di lettura in più righe,
incolonnando opportunamente i test) costituirà
il corpo della procedura del programma. Ecco il programma completo:
(define seleziona_A
(lambda (n)
(or (and (<= n 100)
(>= n 40)
(= 0 (remainder n 2)) )
(> n 1000) )))
Esercizio 3.6:
Scrivere un programma che seleziona tutti i numeri
dell'insieme B, definito come il complementare dell'insieme
A dell'esercizio precedente (ossia: a B
appartengono tutti e soli i numeri che non appartengono ad A).
Sol. Possiamo scrivere l'insieme B come segue
(anche se non è rilevante ai fini
della soluzione dell'esercizio):
B = {..... -10, -9, -8, .... , 37, 38, 39, 41, 43, 45, 47, ....
95, 97, 99, 101, 102, 103, 104, .... , 997, 998, 999, 1000}
La soluzione dell'esercizio è molto semplice, avendo
a disposizione il programma dell'esercizio precedente. È
infatti sufficiente prendere il test di Seleziona_A e farne
la negazione. Il nuovo test darà come risposte valori
di verità opposti al test precedente: false, se n appartiene
ad A (ossia se n non appartiene a B); true, se n non appartiene ad A
(ossia se n appartiene a B). Il programma completo è pertanto:
(define seleziona_B
(lambda (n)
(not (or (and (<= n 100)
(>= n 40)
(= 0 (remainder n 2)) )
(> n 1000) ))))
Avendo a disposizione seleziona_A, possiamo naturalmente abbreviare
il codice di seleziona_B:
(define seleziona_B
(lambda (n)
(not (seleziona_A n))))
Espressioni condizionali
Le espressioni condizionali,
nel contesto di altri linguaggi di programmazione chiamate talvolta
espressioni “if-then-else”, danno la possibilità
di operare delle scelte. In Scheme sono possibili due
tipi differenti di sintassi per le espressioni condizionali.
Ci soffermeremo solo su una di queste, la sintassi delle cosiddette
“espressioni-cond”.
Le espressioni-cond consistono di una serie di test e di conseguenze
(una per ciascun test), e di una alternativa finale.
La sintassi è la seguente:
(cond (<test_1> <conseguenza_1>)
(<test_2> <conseguenza_2>)
.....
(<test_n> <conseguenza_n>)
(else <alternativa_finale>) )
La valutazione dell'intera espressione avviene come segue:
viene prima valutato il primo test, <test_1>: se questo
è vero, si valuta <conseguenza_1>, mentre tutto
il resto della espressione non viene considerato.
Se invece <test_1> è falso, si passa a valutare
<test_2>: se questo è vero, si valuta
la sua conseguenza <conseguenza_2>, e le righe
successive dell'espressione-cond non vengono considerate.
Se anche <test_2> dà false come risultato,
si passa al test successivo, e così via, fino a che
non si trova il primo test che dà esito true, che
consentirà di valutare la relativa conseguenza.
Se, cosa possibile, nessun test dà esito true, si passerà
a valutare
l'alternativa finale, scritta nell'ultima riga della espressione
e preceduta dalla parola chiave “else”.
Esercizio 3.7: Scrivere un programma
“quoto_2”
che, applicato ad un intero n, scrive il quoto della divisione
di n con 2, nel caso in cui n sia pari. Se invece n è
dispari, il programma restituisce il quoto della divisione
di n+1 con 2
(ricordiamo che si ha quoto in una divisione intera, quando
il resto della divisione è 0, altrimenti il risultato
viene chiamato quoziente; la procedura utilizzata per calcolare
quoto o quoziente, come noto, è “remainder”).
Sol. Il corpo della procedura è
costituito da una espressione condizionale che fa uso di
un unico test (con relativa alternativa).
- nome assegnato al programma: quoto_2
- nomi assegnati ai parametri: n
- corpo della procedura:
(cond ( (= 0 (remainder n 2)) (quotient n 2) )
(else (quotient (+ n 1) 2)) )
Ecco il programma completo:
(define quoto_2
(lambda (n)
(cond ((= 0 (remainder n 2)) (quotient n 2) )
(else (quotient (+ n 1) 2)) ) ))
Esercizio 3.8:
Nel paese X è entrato in vigore un nuovo regime di tassazione:
un reddito viene tassato del 8% se è uguale o superiore ai 50000 euro.
Se invece è inferiore, subisce una tassazione del 3%.
Sol.
Questo esercizio è simile all'Esercizio 8, con la complicazione
di dovere operare una scelta a seconda che il reddito sia inferiore
o meno ai 50000 euro. Se chiamiamo r il reddito da tassare, avremo
due formule differenti, a seconda di dove si situa il reddito.
Se r < 50000, calcoleremo il reddito netto con la formula (in Scheme)
(- r (* (/ 3 100) r))
Altrimenti, se r>= 50000, calcoleremo il reddito netto con la formula
(- r (* (/ 8 100) r))
Ecco l'espressione condizionale che utilizzeremo:
(cond ( (< r 50000) (- r (* (/ 3 100) r)) )
( else (- r (* (/ 8 100) r)) )
Chiamiamo il programma finale tassa_2. Ecco il codice:
(define tassa_2
(lambda (r)
(cond ((< r 50000) (- r (* (/ 3 100) r)) )
(else (- r (* (/ 8 100) r)) ))))
Esercizio 3.9: (variazione dell'esercizio
precedente) Nel paese X viene proposta una modifica
al regime di tassazione secondo il seguente schema:
- reddito inferiore a 100000 euro, tassa del 3%
- reddito compreso fra 100000 e 199999 euro, tassa del 8%
- reddito compreso fra 200000 e 299999 euro, tassa del 13%
- reddito compreso fra 300000 e 399999 euro, tassa del 18%
- reddito maggiore o uguale a 400000 euro, tassa del 23%
Scrivere un programma che, dato un reddito lordo, calcola il reddito
netto.
Sol.
Siccome il calcolo del reddito netto, in tutti i casi,
fa uso della stessa formula, (cambia solo la
tassa applicata), potremmo preliminarmente scrivere un programma
ausiliario “applica_tassa” che, presi come argomento
un reddito lordo r e una tassa percentuale t, applica la tassa t ad r e calcola
il reddito netto.
La formula per il calcolo del reddito netto è
reddito_netto = r - (t/100)*r
Ecco il codice di questo programma:
(define applica_tassa
(lambda (r t)
(- r (* (/ t 100) r))))
Ciò che ora faremo è tradurre in Scheme,
attraverso una espressione-cond, la tabella di tassazione.
Nello scrivere i test, terremo conto del fatto che, per passare
a valutare un test successivo, è necessario che il test
precedente non sia stato superato. Ad esempio, quando
scriveremo il secondo test (reddito compreso fra
100000 e 199999 euro) non servirà chiedere se r
è maggiore o uguale di 100000: se si arriva
al secondo test, vuol dire che il primo ha già dato
esito false, per cui sarà necessariamente r >= 100000.
Ecco la traduzione della tabella (si noti il richiamo ad applica_tassa)
(cond ( (< r 100000) (applica_tassa r 3) )
( (< r 200000) (applica_tassa r 8) )
( (< r 300000) (applica_tassa r 13) )
( (< r 400000) (applica_tassa r 18) )
( else (applica_tassa r 23) ) )
Chiamiamo tassa_3 il programma finale. Eccone il codice:
(define tassa_3
(lambda (r)
(cond ((< r 100000) (applica_tassa r 3) )
((< r 200000) (applica_tassa r 8))
((< r 300000) (applica_tassa r 13))
((< r 400000) (applica_tassa r 18))
(else (applica_tassa r 23) ) ) ))
Programmi ricorsivi
Abbiamo visto nella precedente sezione che è possibile,
all'interno di un programma, richiamare altri programmi, al fine
di ottimizzare la scrittura del codice, evitare possibili errori,
e rendere il programma di più agevole lettura.
Una ulteriore possibilità offerta da Scheme (e da altri
li nguaggi di programmazione), di grande interesse teorico, è
la facoltà di richiamare ricorsivamente,
all'interno di un programma, lo stesso
programma che si sta definendo. Queste definizioni
“circolari” richiedono particolari attenzioni, in quanto
possono generare i cosiddetti “loop” infiniti.
Se usate propriamente, conferiscono grande potenza espressiva
al linguaggio. Le definizioni ricorsive di programmi emergono
in modo naturale quando, eccezion fatta per
uno o più casi base (a cui la computazione
dovrà prima o poi
essere ricondotta), un dato processo di computazione
è scomponibile in
sottoprocessi “simili” al processo stesso.
Illustriamo con un esercizio questa situazione.
Esercizio 4.1: Si scriva un programma in
Scheme, chiamato “fattoriale”, che, assegnato un numero
intero positivo n (per il momento strettamente maggiore di 0),
produce come risultato il prodotto di tutti i numeri compresi fra 1
ed n (con il nome “fatt” indicheremo
la funzione matematica corrispondente: fatt(n) = 1*2*...*n).
Sol. Dal programma fattoriale ci attendiamo ad esempio questi
risultati:
(fattoriale 1) → 1
(fattoriale 2) → 2 (= 1*2)
(fattoriale 3) → 6 (= 1*2*3)
(fattoriale 4) → 24 (= 1*2*3*4) etc
Ragionando sul processo di calcolo che consente di calcolare
la funzione fattoriale di un qualunque numero n, osserviamo che
- se n=1 (caso base), il risultato è 1
- se n>1, il calcolo di (fattoriale n) può
essere scomposto in due passaggi:
- calcolare il fattoriale del numero precedente (n-1) ...
- ...e moltiplicarlo per n
Ad esempio:
fatt(2) = 1*2 = fatt(1)*2
fatt(3) = 1*2*3 = (1*2)*3 = fatt(2)*3
fatt(4) = 1*2*3*4 = (1*2*3)*4 = fatt(3)*4
fatt(5) = 1*2*3*4*5 = (1*2*3*4)*5 = fatt(4)*5
...etc...
In sintesi, scrivendo in Scheme caso base e formula ricorsiva, abbiamo,
per la funzione fatt, il seguente schema:
-
fatt(1) = 1 (caso base: n=1)
-
fatt(n) = fatt(n-1) * n (se n>1)
Si osservi che lo schema sopra contiene tutta l'informazione
necessaria per il calcolo del fattoriale di un numero.
La formula relativa al caso baso è esplicita: a secondo
membro si trova un risultato perfettamente definito.
Invece, la formula relativa al caso n>1 è implicita,
poiché il secondo membro contiene nuovamente un riferimento
alla funzione fatt che dobbiamo calcolare.
Ciò costringe (come accade ai programmi ricorsivi di Scheme) a
effettuare la cosiddetta chiamata ricorsiva quando si
procede alla computazione.
Lo schema che definisce la funzione fatt richiama se stesso un numero
di volte sufficiente fino a raggiungere il caso base, dove,
finalmente, la computazione risultante potrà essere svolta
senza un ulteriore chiamata ricorsiva.
Ad esemplificare quanto detto, procediamo a calcolare
(fattoriale 4) utilizzando lo schema. In quanto segue sotto,
solo nell'ultima riga l'espressione sarà stata resa
esplicita; nelle righe precedenti si renderà necessaria
la chiamata ricorsiva:
fatt(4) =
= fatt(3) * 4
= (fatt(2) * 3) * 4
= (fatt(1) * 2) * 3 * 4
= 1*2*3*4 = 24
Sintetizzando, abbiamo a disposizione due definizioni della funzione
fatt, che portano a calcolare gli stessi risultati
(una avvertenza: nella definizione ricorsiva abbiamo usato una notazione spuria,
che mescola espressioni scritte nell'usuale formato matematico
con le espressioni-cond di Scheme):
Prima definizione (esplicita):
fatt(n) = 1*2*...*n
Seconda definizione (ricorsiva):
fatt(n) =
(cond ((n = 1) 1)
(else fatt(n-1) * n) )
Abbiamo visto che la definizione ricorsiva, esplicitando
via via le espressioni ottenute, consente di arrivare al risultato
che la finizione esplicita porge immediatamente.
La definizione ricorsiva ha quindi l'evidente svantaggio di essere meno
immediata di quella esplicita. D'altra parte, almeno a livello
di scrittura delle formule,
presenta il vantaggio di coinvolgere, nella formula, UNA sola operazione
di moltiplicazione, anziché parecchie
(n - 1 moltiplicazioni, se l'argomento è n, nel caso
della definizione esplicita). Questa economia di mezzi
si riflette in programmi brevi e comprensibili, mentre lo
svantaggio sopra menzionato viene totalmente mascherato dal
fatto che è la macchina, e non l'uomo, deputata
a esplicitare le espressioni attraverso
le chiamate ricorsive (il difetto può in realtà
avere conseguenze in termini di efficienza dei programmi;
non tratteremo tuttavia questo argomento).
Possiamo ora scrivere il programma fattoriale completo.
Il corpo della procedura consiste nella traduzione in Scheme
dello schema ricorsivo riportato sopra per la funzione fatt:
- nome assegnato al programma: fattoriale
- nome assegnato al parametro: n
- corpo della procedura:
(cond ( ( = n 1) 1)
(else (* (fattoriale (- n 1)) n) ))
Ecco il programma completo:
(define fattoriale
(lambda (n)
(cond (( = n 1) 1)
(else (* (fattoriale (- n 1)) n) )) ))
Esercizio 4.2:
Si scriva un programma “pot” che, assegnate una base
B ed un esponente intero positivo e, calcola la potenza Be.
Sol.
Come noto, Be corrisponde al prodotto B*B*....*B, dove i
fattori B compaiono e volte (se e=0, si pone B0=1.
Tuttavia, come nel caso della funzione fattoriale vista nel precedente
esempio, possiamo calcolare Be in modo differente, rendendo
minime le operazioni di moltiplicazione, attraverso
la formula (for-pot) introdotta sotto (usiamo la notazione con formato misto matematico/espressioni-cond di Scheme):
(for-pot) |
Be
= (cond ((= e 0) 1)
(else B(e-1) * B))
|
Testiamo la correttezza di (for-pot), provando
a calcolare attraverso essa B4 e verificando
che il risultato viene effettivamente B*B*B*B. Avremo:
B4 =
= B3 * B
= (B2 * B) * B
= (B1 * B) * B * B
= (B0 * B) * B * B * B
= 1*B*B*B*B
= B*B*B*B
(for-pot), come si può osservare, è una formula
ricorsiva: il calcolo della potenza, relativa all'esponente e,
è ricondotto, ogniqualvolta e>0, al calcolo
della potenza con esponente e-1, che a sua volta richiamerà
il calcolo della potenza con esponente e-2, e così via
fino a che non si arriverà ad esponente 0, che segnerà
il termine della chiamata ricorsiva.
Prepariamo lo schema che precede la stesura del programma.
Si osservi che il corpo della procedura consiste in (for-pot)
integralmente tradotta in Scheme:
- nome assegnato al programma: pot
- nomi assegnati ai parametri: B, e
- corpo della procedura:
(cond ( ( = e 0) 1)
(else (* (pot B (- e 1)) B) ))
Ecco infine il programma:
(define pot
(lambda (B e)
(cond (( = e 0) 1)
(else (* (pot B (- e 1)) B) )) ))
Esercizio 4.3: Indichiamo con
σ(n) la somma dei numeri che vanno da 0 fino a n
(esempi: σ(3) = 0 + 1 + 2 + 3 = 6; σ(0) = 0;
σ(5) = 0 + 1 + 2 + 3 + 4 + 5 = 15). Si scriva un programma ricorsivo
SOMMATORIA che calcoli la funzione σ.
Sol.
Premettiamo che è possibile risolvere questo esercizio
attraverso la formula
σ(n) = n*(n+1)/2
Non consideriamo tuttavia questa possibilità e procediamo
a dare una soluzione ricorsiva.
Osserviamo che σ(n), per n>0, è riconducibile
al calcolo di σ(n-1). Abbiamo ad esempio.
σ(4) = 0 + 1 + 2 + 3 + 4
= (0 + 1 + 2 + 3) + 4
= σ(3) + 4
In generale, vale la formula (per n>0):
σ(n) = &sigma(n-1) + n
La formula sopra, unitamente al caso base, σ(0) = 0,
consente di scrivere il programma, che presentiamo senza
ulteriori commenti:
(define SOMMATORIA
(lambda (n)
(cond ((= n 0) 0)
(else (+ (SOMMATORIA (- n 1)) n)))))
Esercizio 4.4: Dato n>0, chiamiamo δ(n)
la somma dei numeri dispari da 1 fino al più grande numero
dispari minore o uguale a n (esempi: δ(5) = 1 + 3 + 5 = 9;
δ(6) = 1 + 3 + 5 = 9; δ(1) = 1). Scrivere un programma
DELTA che calcola la funzione δ.
Sol.
Il problema è simile a quello del precedente esercizio,
ma richiede di analizzare più casi.
Per prima cosa prendiamo in considerazione alcuni esempi, che ci
consentiranno di arrivare alla formula ricorsiva per δ.
δ(7) = 1 + 3 + 5 + 7
= (1 + 3 + 5) + 7
= δ(5) + 7
δ(8) =
1 + 3 + 5 + 7
= δ(7)
δ(11) = 1 + 3 + 5 + 7 + 9 + 11
= (1 + 3 + 5 + 7 + 9) + 11
= δ(9) + 11
δ(12) = 1 = 3 + 5 + 7 + 9 + 11
= δ(11)
Avremo in generale:
- un caso base: n = 1, con risultato δ(1) = 1
- il caso n>1, con n dispari. In questo caso avremo
δ(n) = δ(n-2) + n
- il caso n>1, con n pari. In questo caso avremo
δ(n) = &delta(n-1)
I tre casi sopra, tradotti in Scheme, costituiscono
il corpo della procedura del nostro programma
(define DELTA
(lambda (n)
(cond ((= n 1) 1)
((= 1 (remainder n 2))
(+ (DELTA (- n 2)) n))
(else (DELTA (- n 1))))))
La “costruzione” delle operazioni aritmetiche
Avvertenza per chi ha seguito il corso: la presente sezione
è sviluppata in modo leggermente semplificato rispetto
a quanto visto a lezione, dove i “mattoni” su cui
costruire i programmi
sono la funzione successore e la funzione predecessore
Sull'insieme dei numeri naturali Nat={0, 1, 2, ...} siamo abituati
a pensare che le operazioni più semplici siano le cosiddette
4 operazioni, i cui calcoli vengono svolti attraverso opportune
computazioni sulle rappresentazioni decimali dei numeri.
In ordine di difficoltà crescente (e quindi di apprendimento):
somma, differenza, prodotto, divisione intera, resto. Differenza e divisione
intera sono soggette alle usuali restrizioni: siccome non abbiamo
a disposizione i numeri negativi, si suppone che in una sottrazione
il diminuendo sia sempre maggiore o uguale al sottraendo,
mentre nel caso della divisione il divisore non potrà mai
essere pari a 0.
Questa sezione ha due obbiettivi:
-
mostrare come le varie operazioni aritmetiche possano
essere definite, attraverso la ricorsione, a partire
dalle due operazioni più semplici: somma +,
e differenza -.
-
fare (ri)emergere le relazioni che legano fra loro le operazioni.
Si tratta di una sezione dove il lettore, oltre a elementi
di novità, incontrerà proprietà delle operazioni
che ben conosce e che potrà vedere all'opera nella
costruzione dei programmi.
Fissiamo ora il punto di partenza da cui muoveremo.
Partiamo dall'ipotesi che:
- possiamo usare solo due operazioni aritmetiche predefinite,
+ e -, mentre ci è negato l'uso di ogni altra operazione
(*, quotient, remainder etc.)
- possiamo scrivere (naturalmente!) programmi in Scheme secondo quanto visto
nelle sezioni precedenti.
Il nostro scopo sarà recuperare tutte le operazioni aritmetiche
che conosciamo: desideriamo, a partire da + e -
- scrivere un programma MUL che si comporta
come *: dati due fattori n ed m, (MUL n m) dovrà dare come
risultato il loro prodotto (ricordate: non possiamo usare
l'operazione predefinita *!);
-
scrivere un programma QUOZIENTE che dato un dividendo n ed un divisore m,
dà come risultato il quoziente della loro divisione (nuovamente:
ci è interdetto l'uso della procedura quotient)
-
scrivere un programma RESTO che dato un dividendo n ed un divisore m,
dà come risultato il resto della loro divisione;
-
scrivere programmi che calcolano altre operazioni sui numeri naturali quali:
l'elevamento a potenza, il massimo comun divisore e così
via.
Costruzione della moltiplicazione
Come sopra delineato, vogliamo un programma MUL che calcola
la moltiplicazione. La strada attraverso cui determineremo MUL,
non potendo utilizzare la procedura *, farà riferimento
direttamente alla definizione di moltiplicazione, vista come
somma di un numero di addendi, tutti uguali al primo fattore, pari
al secondo fattore: n * m = n + n + n + ... + n + n (m addendi).
Questa somma si presta subito a essere scritta ricorsivamente.
Separando infatti i primi m-1 addenti dall'ultimo addendo, otteniamo:
n + n + n + .... + n + n (m addendi)
= (n + n + n + ... + n) + n (in parentesi m-1 addendi)
= (n * (m-1)) + n
Il caso base, ottenuto per m=0, (ossia: n*0 = 0),
unitamente a quanto visto sopra, ci consente di determinare
una formula ricorsiva attraverso cui definire la moltiplicazione.
Esprimiamo tale formula direttamente in Scheme:
(for-mul) |
(MUL n m)
= (cond ((= m 0) 0)
(else (+ (MUL n (- m 1)) n)
|
Siamo ora in grado di scrivere integralmente in programma MUL;
il corpo della procedura consisterà nel secondo membro
della uguaglianza (for-mul).
(define MUL
(lambda (n m)
(cond ((= m 0) 0)
(else (+ (MUL n (- m 1)) n)))))
Costruzione del resto
A lezione, oltre a quello qua presentato,
è stato spiegato un secondo programma per il calcolo
del resto, basato sulle funzioni successore e predecessore.
Cerchiamo ora un programma RESTO che calcoli il resto della divisione
intera fra n ed m (con m diverso da 0). Ancora una volta, la scrittura
del programma si baserà su note proprietà della divisione
intera (e del resto).
Dati n ed m numeri naturali, con m diverso da 0, procediamo alla divisione
intera di n con m, e chiamiamo q il quoziente ed r il resto.
Avremo allora
n = m*q + r
r deve esattamente essere il risultato dato da (RESTO n m).
Come noto, il resto r è un valore maggiore o uguale a 0
e strettamente minore del divisore m.
Se n è un numero minore di m, r è banalmente uguale
a n medesimo, mentre il quoziente q vale 0. In questo caso vale
infatti l'uguaglianza
n = m*0 + n
Supponiamo ora n maggiore o uguale a m.
In questo caso r viene (in genere)
calcolato congiuntamente a q: q è il più grande numero tale che
il prodotto m*q si mantiene minore o uguale al dividendo n (pertanto:
m*(q+1) sarà maggiore di n), mentre
r è la differenza fra n e m*q.
Tuttavia noi non procederemo seguendo questo schema.
Piuttosto, osserviamo la seguente proprietà:
il resto della divisione fra n ed m è uguale
al resto della divisione fra (n-m) ed m
Infatti, partendo dall'ipotesi n = m*q + r, abbiamo
n - m = m*q + r - m
= m*q - m + r
= m*(q-1) + r
Come si vede, il resto della divisione fra n-m ed m non è variato
rispetto alla divisione fra n ed m (mentre è variato il
quoziente, ora uguale a q-1).
Possiamo raccogliere quanto ora vist nella seguente formula:
(for-resto) |
(resto n m)
= (cond ((< n m) n)
(else (resto (- n m) m))
|
Prima di scrivere il programma RESTO completo, vediamo un esempio
di applicazione di (for-resto) al calcolo del resto della divisione
intera fra 40 (divisore) e 7 (dividendo).
(resto 40 7) =
= (resto 33 7)
= (resto 26 7)
= (resto 19 7)
= (resto 12 7)
= (resto 5 7)
= 5
Procediamo ora alla scrittura del programma completo, di
cui (for-resto) costituisce il corpo della procedura:
(define RESTO
(lambda (n m)
(cond ((< n m) m)
(else (resto (- n m) m)))))
Costruzione del quoziente
A lezione, oltre a quello qua presentato,
è stato spiegato un secondo programma per il calcolo
del quoziente, basato sulle funzioni successore e predecessore.
Cerchiamo ora un programma QUOZIENTE che calcoli il quoziente della divisione
intera fra n ed m (con m diverso da 0). Possiamo sfruttare le considerazioni
viste nel punto precedente, relative al programma RESTO:
quando n è minore di m, il quoziente della divisione vale 0.
Altrimenti, come osservato precedentemente, sappiamo che il quoziente
della divisione fra n ed m è uguale a 1 più il quoziente
della divisione fra n-m ed m. Sintetizzando
(for-quoz) |
(quoziente n m)
= (cond ((< n m) 0)
(else (+ 1 (quoziente (- n m) m)))
|
Prima di scrivere il programma QUOZIENTE completo, vediamo un esempio
di applicazione di (for-quoz) al calcolo del resto della divisione
intera fra 40 e 7.
(quoziente 40 7) =
= 1 + (quoziente 33 7)
= 1 + (1 + (quoziente 26 7))
= 1 + (1 + (1 + (quoziente 19 7)))
= 1 + (1 + (1 + (1 + (quoziente 12 7))))
= 1 + (1 + (1 + (1 + (1 + (quoziente 5 7)))))
= 1 + (1 + (1 + (1 + (1 + 0))))
= 5
Procediamo ora alla scrittura del programma QUOZIENTE completo, di cui
(for-quoz) costituirà il corpo della procedura:
(define QUOZIENTE
(lambda (n m)
(cond ((< n m) 0)
(else (+ 1 (QUOZIENTE (- n m) m))))))
Costruzione dell'elevamento a potenza
Questo programma è già stato visto. Riportiamo
solo il codice senza commenti, sostituendo, per coerenza
con le restrizioni della presente sezione, la procedura Scheme *
con il programma MUL.
(define POT
(lambda (B e)
(cond (( = e 0) 1)
(else (MUL (POT B (- e 1)) B) )) ))
Costruzione del Massimo Comun Divisore
Il prossimo programma, che chiameremo MCD, calcola il massimo
comun divisore di due numeri n ed m, che chiamiamo mcd(n,m).
Un metodo per calcolare mcd(n,m)
consiste nello scomporre in fattori
primi n ed m, e calcolare il prodotto dei fattori comuni, presi
con l'esponente minore con cui compaiono nelle due scomposizioni.
Esiste tuttavia un altro algoritmo, che è fra l'altro legato a quanto
visto nei punti precedenti relativi ai programmi RESTO e QUOZIENTE.
mcd(n,m), per definizione, è il più grande dei divisori
comuni di n ed m. Consideriamo due coppie di numeri: (n,m) e (a,b).
Se accade che l'insieme dei divisori comuni di n ed m coincide
con l'insieme dei divisori comuni di a e b, allora dovrà essere
mcd(n,m) = mcd(a,b). Ad esempio: se abbiamo n=30, m=24, a=60, b=78,
si ha che l'insieme dei divisori comuni di 30 e 24 è
A = {1, 2, 3, 6}
Similmente, A è anche l'insieme dei divisori comuni di 60 e 78.
È pertanto ovvio che mcd(30,24) = mcd(60,78) = 6.
Ora, si osservi che, dati n ed m, con n maggiore di m, l'insieme
dei divisori comuni di n ed m è esattamente lo stesso
dell'insieme dei divisori comuni di n-m ed m.
Vediamo un esempio di questa proprietè: sia n=105, m=75.
L'insieme dei divisori comuni di n ed m è in questo caso
B = {1, 2, 3, 5, 15}
D'altra parte, come si può facilmente verificare,
30 (= n-m) e 75 continuano ad avere lo stesso insieme B
di divisori, e pertanto dovrà essere
mcd(105,75) = mcd(30,75) (= 15).
La giustificazione di questa proprietà è
in sostanza equivalente alle seguente semplice osservazione:
se un numero p divide sia n che m,
allora divide anche la loro differenza;
d'altra parte, se un numero p divide sia n-m che m, allora divide anche n
(provate a dimostrare entrambi i fatti!).
In conclusione si avrà (sempre nell'ipotesi n > m)
mcd(n,m) = mcd(n-m,m)
Le stringhe
Con il termine “stringa” intendiamo una successione
di caratteri alfa-numerici, racchiusi fra doppio apice.
Ecco alcuni esempi di stringhe:
"a12-bc" "asdf_+=lksa"
"Anna" "Benvenuti al corso di IAD"
"" " "
La penultima stringa scritta "", che non contiene alcun carattere,
(neanche la spaziatura),
si chiama stringa vuota, ed è differente dall'ultima
stringa scritta,
" ", che contiene un carattere (la spaziatura).
Nel prosieguo della discussione, le sequenze “nude” di caratteri
(non racchiuse fra doppi apici) verranno spesso indicate con lettere
greche: σ, τ etc. La convenzione ora introdotta
(che riguarda NON la sintassi del linguaggio Scheme,
ma solo la presente esposizione)
consente di riferirsi a generiche successioni di caratteri
in modo compatto, attraverso un unico simbolo.
Vediamo una applicazione di questa convenzione.
Se associamo α alla sequenza di caratteri Anna,
"α" sarà un modo sintetico per riferirsi alla stringa
"Anna". Analogamente, per una qualunque sequenza di caratteri
σ, "σ" sarà la stringa costituita da quella
sequenza di caratteri.
Sequenze nude di caratteri e stringhe sono concetti
che non vanno confusi: le sequenze nude
costituiscono gli
identificatori, ossia quei nomi che, nelle definizioni,
vengono assegnati a espressioni (costanti, programmi), e
nelle procedure, ai parametri
(ad esempio, si ricordi la definizione
(define pi_greco 3.14); gli identificatori assumono il significato che a loro
attribuisce la relativa definizione, o che possono,
introdotti da un lambda, essere impiegati come parametri
all'interno delle procedure.
Viceversa, le stringhe sono oggetti definiti e non
hanno lo scopo di venire associate ad altro.
Passiamo a qualche esempio che illustra ulteriormente
la differenza fra stringhe ed identificatori.
Supponiamo di non avere definito nulla nella finestra
delle definizioni di Dr. Scheme. Se nella Interaction Window
digitiamo "Anna" e clicchiamo Invio, il risultato sarà "Anna".
Se digitiamo Anna e clicchiamo Invio, il risultato sarà un
messaggio di errore, in quanto Anna è un identificatore
a cui non è stato associato nessun oggetto (attraverso una
definizione).
Se nella finestra delle definizioni diamo la definizione seguente
(define Anna (+ 3 4))
clicchiamo Run (o Execute) e poi, dopo avere digitato nella finestra delle interazioni
Anna, clicchiamo Invio, il risultato che otterremo sarà il numero 7,
poichè la nostra definzione attribuisce all'identificatore
Anna (il risultato del)la espressione (+ 3 4).
Se viceversa digitiamo "Anna" e clicchiamo Invio, il risultato
continuerà a essere la stringa "Anna".
Infine, se nella finestra delle definizioni diamo questa definizione
(define "Anna" (+ 1 2))
e clicchiamo Run, otterremo subito un messaggio di errore,
poichè "Anna", munita del doppio apice, è una stringa,
non un identificatore, e quindi non può essere utilizzata
in una definizione come nome da associarsi ad una espressione.
Alle stringhe si applicheremo 4 procedure predefinite:
- string-length
- string-append
- substring
- string=?
La procedura string-length calcola la lunghezza di una stringa.
Esempi:
(string-length "Anna") -> 4
(string-length "") -> 0
(string-length "CiaoPaola") -> 9
(string-length "Ciao Paola") -> 10
(string-length "Ciao Paola") -> 11 (le spaziature contano)
(string-length " ") -> 1
L'unica stringa ad avere lunghezza 0 è la stringa vuota "".
Di lunghezza pari a 1 sono tutte le stringhe costituite da un solo
carattere, che chiameremo singoletti (esempi: "a" "1" "x" etc,
ma non "a ", " 1 " etc).
La procedura string-append prende come argomenti un numero arbitrario
di stringhe e ne fa la concatenazione (non creando spaziature ulteriori fra le
stringhe). Ecco qualche esempio:
(string-append "Ciao" "Anna!") -> "CiaoAnna!"
(string-append "Ciao " "Anna!") -> "Ciao Anna!"
(string-append "Ci" "ao" " An" "na" "come " "va?") ->
"Ciao Annacome va?")
(string-append "Anna" " " "Paola") -> "Anna Paola"
(string-append "Anna" "") -> "Anna"
(string-append "casa") -> "casa"
(string-append "12" "34") -> "1234"
(string-append 12 34) -> messaggio di errore
Si osservino in particolare gli ultimi due esempi. L'operazione di
concatenare le stringhe "12" e "34" (si ricordi:
nonostante l'apparenza, questi non sono numeri!!)
è perfettamente lecita, mentre la concatenazione dei due numeri
12 e 34 non ha senso (ai numeri applichiamo un altro set di operazioni:
+, -, quotient, * etc etc)
La procedura substring estrae una sottostringa da una stringa data s.
Oltre alla stringa s, occorre specificare due indici, che segnano,
rispettivamente l'inizio (leggendo da sinistra a destra)
della stringa da estrarre e il primo indice a destra oltre la
stringa (pertanto il carattere che avrà questo indice
risulterà essere al di fuori della stringa estratta).
Si ricordi che gli indici si iniziano a contare a partire dal numero 0,
e come valore massimo (è importante ricordarlo) possono assumere
la lunghezza della stringa, che corrisponde alla posizione che decreta
il termine della stringa.
Vediamo un esempio. Si consideri la stringa "violino". Ecco la corrispondenza
indici/caratteri:
- indice 0 -> v
- indice 1 -> i
- indice 2 -> o
- indice 3 -> l
- indice 4 -> i
- indice 5 -> n
- indice 6 -> o
- indice 7 -> fine della stringa
Il primo carattere di una stringa corrisponderà sempre
all'indice 0 (qualunque sia la stringa).
Nell'esempio riportato, all'ultima "o" di "violino"
corrisponde l'indice 6, che è uguale alla lunghezza della
stringa "violino" (pari a 7) meno 1. In generale, data una qualunque
stringa s non vuota, l'ultimo carattere della stringa in questione
si otterrà in corrispondenza dell'indice
(- (string-length s) 1), ossia lunghezza della stringa s meno 1.
Vediamo ora come estrarre sottostringhe dalla stringa "violino".
Se desideriamo estrarre la sottostringa "iolin", dovremo selezionare
l'indice 1, che corrisponde alla prima "i", e l'indice 6, che corrisponde
al primo indice a destra oltre la sottostringa da estrarre:
(substring "violino" 1 6) -> "iolin"
Se desideriamo estrarre la sottostringa finale "lino", dovremo
selezionare l'indice corrispondente alla "l" (3), e l'indice
immediatamente oltre la fine della stringa (7, che corrisponde
alla lunghezza della stringa, e, come scritto sopra, segna
la fine della stringa).
(substring "violino" 3 7) -> "lino"
Se desideriamo estrarre la sottostringa singoletto costituita
unicamente dal primo carattere di "violino", la "v",
dovremo selezionare l'indice corrispondente al primo (e unico)
carattere della sottostringa (il carattere iniziale, quindi indice 0),
e l'indice successivo (l'indice 1, collocato immediatamente
a destra del termine della stringa da estrarre):
(substring "violino" 0 1) -> "v"
Se desideriamo estrarre la sottostringa singoletto costituita
unicamente dall'ultimo carattere di "violino", la "o" finale,
dovremo selezionare l'indice corrispondente al primo (e unico)
carattere della sottostringa (il carattere finale, quindi indice
6 = (- (string-length "violino") 1) ), e l'indice successivo
(l'indice 7 = (string-length "violino"), che segna come detto
il termine della stringa):
(substring "violino" 6 7) -> "o"
Infine, osserviamo che la sottostringa estratta da "violino"
impiegando gli indici
0 e 7, coincide esattamente con la stringa di partenza:
(substring "violino" 0 7) -> "violino"
La procedura string=? costituisce il test di uguaglianza fra stringhe:
prese due stringhe come argomento, restituisce come risultato un valore
di verità, true oppure false, a seconda che i due argomenti
siano esattamente uguali o meno (per quanto riguarda le lettere, le
maiuscole sono distinte dalle minuscole):
(string=? "Anna" "anna") -> false
(string=? "Anna" "Anna") -> true
(string=? "pane" " pane") -> false
(string=? "" " ") -> false
Il test string=? non può essere sostituito dal test di uguaglianza
fra numeri =: il test di uguaglianza fra stringhe va
tassativamente fatto utilizzando string=?. Pertanto un test del tipo
(= "Anna" "pane")
produrrà un messaggio di errore.
Nonostante il test string=? possa al momento sembrare destinato
ad un utilizzo poco più che banale, se ne coglierà
fra poco l'importanza, allorché verrà impiegato
utilizzando un argomento parametrico.
Passiamo ora a qualche semplice programma sulle stringhe.
Esericizio 6.1:
Il programma CIAO, applicato a nomi (di persona), produce risultati
come da seguenti esempi:
(CIAO "Anna") -> "Ciao Anna! Come va?"
(CIAO "Giovanni) -> "Ciao Giovanni! Come va?"
(CIAO "Laura") -> "Ciao Laura! Come va?"
Scrivere un possibile codice Scheme del programma CIAO.
Sol.
Il programma CIAO dovrà prendere in ingresso una stringa
generica "σ", e produrre come risultato
"Ciao σ! Come va?", che possiamo comporre
utilizzando la procedura string-append:
"Ciao σ! Come va?" ≡
(string-append "Ciao " "σ" "! Come va?")
Quanto ora visto ci consente di scrivere il programma:
(define CIAO
(lambda (s)
(string-append "Ciao " s "! Come va?")))
Si osservi che il parametro s verrà sostituito da una stringa
generica "σ" (ad esempio, negli esempi del testo dell'esercizio,
l'argomento σ assume come valori
le sequenze Anna, Giovanni e Laura). Pertanto, essendo
l'argomento s una stringa a tutti gli effetti, già munita
di doppio apice, non è corretta, ad esempio, questa
versione del programma:
(define CIAO-2
(lambda (s)
(string-append "Ciao " "s" "! Come va?")))
(si osservi in proposito che (CIAO-2 "Anna") -> "Ciao s, come va?").
Esercizio 6.2:
Si scriva un programma PRIMO che, data una stringa non vuota s,
estrae la sottostringa singoletto costituita unicamente dal primo
carattere di s.
Sol.
Ecco alcuni esempi di applicazioni di PRIMO:
(PRIMO "Anna") -> "A"
(PRIMO "fuoco") -> "f"
(PRIMO " Ciao!") -> " "
Il programma PRIMO si scrive utilizzando la procedura predefinita
substring, tenendo conto che il primo carattere della stringa
corrisponde all'indice 0, e che il primo indice al di fuori della
sottostringa, a destra, è l'indice 1. Ecco pertanto il programma:
(define PRIMO
(lambda (s)
(substring s 0 1)))
Esercizio 6.3
Si scriva un programma ULTIMO che, data una stringa non vuota s,
estrae la sottostringa singoletto costituita unicamente dall'ultimo
carattere di s.
Sol.
Ecco alcuni esempi di applicazioni di ULTIMO:
(ULTIMO "Anna") -> "a"
(ULTIMO "fuoco") -> "o"
(ULTIMO " Ciao!") -> "!"
Il programma ULTIMO si scrive utilizzando di nuovo substring,
tenendo conto che l'ultimo carattere della stringa corrisponde
all'indice (- (string-length s) 1), e che il primo indice
al di fuori della sottostringa, a destra,
è (string-length s). Ecco il programma:
(define ULTIMO
(lambda (s)
(substring s (- (string-length s) 1)
(string-length s))))
Esercizio 6.4
Si scriva un programma C-PRIMO (abbreviazione per "cancella primo"),
che, data una stringa non vuota s, estrae la sottostringa
ottenuta da s cancellando il primo carattere.
Sol.
Ecco alcuni esempi di applicazioni di C-PRIMO:
(C-PRIMO "Anna") -> "nna"
(C-PRIMO "fuoco") -> "uoco"
(C-PRIMO " Ciao!") -> "Ciao!"
Il programma C-PRIMO si scrive usando substring, tenendo conto
che la sottostringa estratta inizia dal secondo carattere
di s, associato all'indice 1, mentre il primo indice al di fuori
della sottostringa, a destra, è
(string-length s). Ecco il programma:
(define C-PRIMO
(lambda (s)
(substring s 1 (string-length s))))
Esercizio 6.5
Si scriva un programma C-ULTIMO (abbreviazione per "cancella ultimo"),
che, data una stringa non vuota s, estrae la sottostringa
ottenuta da s cancellando l'ultimo carattere.
Sol.
Ecco alcuni esempi di applicazioni di C-ULTIMO:
(C-PRIMO "Anna") -> "Ana"
(C-PRIMO "fuoco") -> "fuoc"
(C-PRIMO " Ciao!") -> " Ciao"
Il programma C-ULTIMO si scrive usando substring, tenendo conto
che la sottostringa estratta inizia dal primo carattere
di s, associato all'indice 0, mentre il primo indice al di fuori
della sottostringa, a destra, è
(- (string-length s) 1), indice che è in corrispondenza
dell'ultimo carattere della stringa. Ecco il programma:
(define C-PRIMO
(lambda (s)
(substring s 0 (- (string-length s) 1))))
Esercizio 6.6
Si scriva un programma FEMMINILE? che, data una stringa, stabilisce,
se questa è associata ad un nome femminile: per questo fine si utilizzi
il seguente algoritmo: un nome è classicifato femminile
se termina con la lettera "a", altrimenti viene classificato
come maschile (naturalmente questo algoritmo, pur funzionando
correttamente in un gran numero di casi, quando
le stringhe sono esprimono nomi italiani di persone,
non dà risposte
corrette sempre, come si può evincere dagli ultimi
esempi sotto). Si desidera infine
che il programma dia come risposta, in caso affermativo, la stringa
"sì, il nome è femminile", mentre in caso negativo
dia come risposta
"no, il nome non è femminile".
Sol.
Ecco alcuni esempi di applicazioni a stringhe specifiche di FEMMINILE?:
(FEMMINILE? "Paola") -> "sì, il nome è femminile"
(FEMMINILE? "Giacomo") -> "no, il nome non è femminile"
(FEMMINILE? "Alessandra") -> sì, il nome è femminile"
(FEMMINILE? "Beatrice") -> "no, il nome non è femminile"
(FEMMINILE? "Luca") -> "sì, il nome è femminile"
(FEMMINILE? "123_sdfkja") -> "sì, il nome è femminile"
Il programma sarà basato su un testo che controlla
se l'ultimo carattere della stringa s è uguale ad "a",
e a seconda dei casi darà la conseguente risposta.
Possiamo utilmente utilizzare il programma ULTIMO all'interno
di FEMMINILE?
(define FEMMINILE?
(lambda (s)
(cond ((string=? "a" (ULTIMO s))
"sì il nome è femminile")
(else "no, il nome non è femminile"))))
Esercizio 6.7
Si scriva un programma FEMMINILE2? che, data una stringa stabilisce,
con i criteri dell'esercizio precendente, se è associata
ad un nome femminile. A differenza del programma precedente, si
richiede che la risposta sia un valore di verità true oppure false.
So.
Ecco alcuni esempi di applicazioni a stringhe specifiche di FEMMINILE2?:
(FEMMINILE2? "Paola") -> true
(FEMMINILE2? "Giacomo") -> false
(FEMMINILE2? "Alessandra") -> true
(FEMMINILE2? "Beatrice") -> false
(FEMMINILE2? "Luca") -> true
(FEMMINILE2? "123_sdfkja") -> true
Rispetto al caso precedente cambia solo la risposta da dare.
Modificando leggermente il programma precedente, possiamo
ottenere la prima versione del programma FEMMINILE2?
(define FEMMINILE2?
(lambda (s)
(cond ((string=? "a" (ULTIMO s))
true)
(else false))))
Si osservi però che il test stesso utilizzato nella procedura
fornisce già la risposta corretta. Il programma può
pertanto essere semplificato come segue:
(define FEMMINILE2?
(lambda (s)
(string=? "a" (ULTIMO s))))
Esercizio 6.8
Diciamo che due nomi sono affini se iniziano con le stesse
due lettere. Ad esempio i nomi "Agostino" ed "Agata" sono affini secondo
questa definizione, e così sono i nomi "Alberto" ed "Alessandra",
mentre non lo sono i nomi "Stefano" e "Susanna". Scrivere un programma
NOMI-AFFINI? che stabilisce se due nomi sono affini, dando, in caso
affermativo risposta "nomi affini", in caso negativo "nomi non affini".
Sol. Si tratta di confrontare le due sottostringhe iniziali
costituite dai primi due caratteri dei nomi, e verificare che queste
sono uguali. Se chiamiamo s e t le due stringhe che dobbiamo
verificare essere affini, le due sottostringhe in questione
le otteniamo con
(substring s 0 2) e (subsrtring t 0 2). Ecco il codice del programma:
(define NOMI-AFFINI?
(lambda (s t)
(cond ((string=? (substring s 0 2) (substring t 0 2)) "nomi affini")
(else "nomi non affini") )))
Esercizio 6.9
Scrivere un programma DIMINUTIVO, che data una stringa s
associata ad un nome, restituisce come il risultato il diminutivo
di quel nome. Si vuole che il programma crei un diminutivo
secondo il seguente schema: se il nome termina con "o", il suffisso
per creare il diminutivo sarà "ino"; se il nome termina
con "a", il suffisso sarà "ina". In tutti gli altri casi
il programma risponderà "non so fare il diminutivo".
Sol. Ecco alcuni esempi di applicazione di DIMINUTIVO:
(DIMINUTIVO "Paolo") -> "Paolino"
(DIMINUTIVO "palla") -> "pallina"
(DIMINUTIVO "scuola") -> "scuolina"
(DIMINUTIVO "Ester") -> "non so fare il diminutivo"
(DIMINUTIVO "Luca") -> "Lucina"
(come si vede dall'ultimo esempio, anche questo programma
non produrrà sempre risposte corrette).
Per scrivere diminutivo dovremo verificare se il nome s
termina con "a", con "o", oppure con un'altra lettera.
In quest'ultimo caso produrremo il messaggio "non so fare
il diminutivo" come risposta. Negli altri due casi, occorrerà
togliere dal nome l'ultimo carattere (ad esempio: da "Paolo",
ricavare "Paol") utilizzando il programma già introdotto
C-ULTIMO, e successivamente concatenare il suffisso corretto
(nel caso del nome "Paolo", concatenare "Paol" con "ino").
Ecco il codice del programma:
(define DIMINUTIVO
(lambda (s)
(cond ((string=? (ULTIMO s) "a")
(string-append (C-ULTIMO s) "ina"))
((string=? (ULTIMO s) "o")
(string-append (C-ULTIMO s) "ino"))
(else "non so fare il dimunitivo"))))
Costruzione della somma
In questa sezione vogliamo scrivere un programma che sommi due numeri,
immaginando di NON avere a disposizione alcuno strumento di calcolo
sui numeri naturali.
Per questo scopo
non lavoreremo direttamente sui numeri naturali 1, 2, 3, etc, ma sulle
loro rappresentazioni decimali, che supporremo essere date dalle
corrispondenti stringhe "1", "2", "3", etc. Operativamente,
la differenza fra i numeri di Scheme
e le loro rappresentazioni decimali è la seguente: ai numeri
è possibile applicare il set di operazioni predefinite visto
all'inizio, quali il test di uguaglianza =, di “essere minore di”
<, “essere maggiore di” >, e poi
+, -, *, remainder etc. Di queste operazioni, nessuna è
applicabile alle rappresentazioni. Essendo
stringhe, potremo usare quelle procedure
che si applicano
a quel tipo di dato: string-length, string-append, substring, string=?,
più i programmi che abbiamo già
introdotto sulle stringhe. In particolare useremo
i programmi PRIMO, C-PRIMO, ULTIMO e C-ULTIMO.
Lavorando con le rappresentazioni numeriche, nessuna usuale
operazioni aritmetica ci è al momento consentita: come visto
in precedenza, non è possibile calcolare ad esempio
(+ "1" "2"). Il nostro obbiettivo sarà costruire un programma
che chiameremo SOMMA che, date le rappresentazioni decimali
di due numeri, produce la rappresentazione
decimale del numero somma dei due. Ad esempio, desideriamo che
(SOMMA "89" "168") -> "257"
Il programma SOMMA verrà costruito attraverso l'algoritmo
che conosciamo per sommare due numeri: mettere le cifre in
colonna, e procedere alle somme delle unità, delle decine, e
così via, tenendo conto dei riporti.
Per prima cosa scriviamo un programma elementare, TABELLINA,
che servirà a calcolare la cifra delle unità in una
somma qualunque. Tale programma prenderà come argomenti
la rappresentazione di due numeri,
ciascuno costituito da una cifra, variabile quindi
da 0 a 9, e produrrà come risultato non la somma
dei numeri, ma semplicemente la cifra delle unità di tale somma.
Vediamo su qualche esempio il funzionamento di TABELLINA:
(TABELLINA "1" "4") -> "5"
(TABELLINA "3" "5") -> "8"
(TABELLINA "8" "9") -> "7"
(TABELLINA "2" "8") -> "0"
TABELLINA è un programma piuttosto noioso da scrivere, dove occorre
contemplare tutte le possibili combinazioni di cifre. Gli unici
casi in cui possiamo compattare i risultati sono i primi due,
nel codice sotto, corrispondenti ai casi in cui una delle due
cifre da sommare sia "0". In questo caso la cifra della somma
coinciderà con l'altra cifra da sommare.
(define TABELLINA
(lambda (s t)
(cond ((string=? s "0") t)
((string=? t "0") s)
((and (string=? s "1") (string=? t "1")) "2")
((and (string=? s "1") (string=? t "2")) "3")
((and (string=? s "1") (string=? t "3")) "4")
((and (string=? s "1") (string=? t "4")) "5")
((and (string=? s "1") (string=? t "5")) "6")
((and (string=? s "1") (string=? t "6")) "7")
((and (string=? s "1") (string=? t "7")) "8")
((and (string=? s "1") (string=? t "8")) "9")
((and (string=? s "1") (string=? t "9")) "0")
((and (string=? s "2") (string=? t "1")) "3")
((and (string=? s "2") (string=? t "2")) "4")
((and (string=? s "2") (string=? t "3")) "5")
((and (string=? s "2") (string=? t "4")) "6")
((and (string=? s "2") (string=? t "5")) "7")
((and (string=? s "2") (string=? t "6")) "8")
((and (string=? s "2") (string=? t "7")) "9")
((and (string=? s "2") (string=? t "8")) "0")
((and (string=? s "2") (string=? t "9")) "1")
((and (string=? s "3") (string=? t "1")) "4")
((and (string=? s "3") (string=? t "2")) "5")
((and (string=? s "3") (string=? t "3")) "6")
((and (string=? s "3") (string=? t "4")) "7")
((and (string=? s "3") (string=? t "5")) "8")
((and (string=? s "3") (string=? t "6")) "9")
((and (string=? s "3") (string=? t "7")) "0")
((and (string=? s "3") (string=? t "8")) "1")
((and (string=? s "3") (string=? t "9")) "2")
((and (string=? s "4") (string=? t "1")) "5")
((and (string=? s "4") (string=? t "2")) "6")
((and (string=? s "4") (string=? t "3")) "7")
((and (string=? s "4") (string=? t "4")) "8")
((and (string=? s "4") (string=? t "5")) "9")
((and (string=? s "4") (string=? t "6")) "0")
((and (string=? s "4") (string=? t "7")) "1")
((and (string=? s "4") (string=? t "8")) "2")
((and (string=? s "4") (string=? t "9")) "3")
((and (string=? s "5") (string=? t "1")) "6")
((and (string=? s "5") (string=? t "2")) "7")
((and (string=? s "5") (string=? t "3")) "8")
((and (string=? s "5") (string=? t "4")) "9")
((and (string=? s "5") (string=? t "5")) "0")
((and (string=? s "5") (string=? t "6")) "1")
((and (string=? s "5") (string=? t "7")) "2")
((and (string=? s "5") (string=? t "8")) "3")
((and (string=? s "5") (string=? t "9")) "4")
((and (string=? s "6") (string=? t "1")) "7")
((and (string=? s "6") (string=? t "2")) "8")
((and (string=? s "6") (string=? t "3")) "9")
((and (string=? s "6") (string=? t "4")) "0")
((and (string=? s "6") (string=? t "5")) "1")
((and (string=? s "6") (string=? t "6")) "2")
((and (string=? s "6") (string=? t "7")) "3")
((and (string=? s "6") (string=? t "8")) "4")
((and (string=? s "6") (string=? t "9")) "5")
((and (string=? s "7") (string=? t "1")) "8")
((and (string=? s "7") (string=? t "2")) "9")
((and (string=? s "7") (string=? t "3")) "0")
((and (string=? s "7") (string=? t "4")) "1")
((and (string=? s "7") (string=? t "5")) "2")
((and (string=? s "7") (string=? t "6")) "3")
((and (string=? s "7") (string=? t "7")) "4")
((and (string=? s "7") (string=? t "8")) "5")
((and (string=? s "7") (string=? t "9")) "6")
((and (string=? s "8") (string=? t "1")) "9")
((and (string=? s "8") (string=? t "2")) "0")
((and (string=? s "8") (string=? t "3")) "1")
((and (string=? s "8") (string=? t "4")) "2")
((and (string=? s "8") (string=? t "5")) "3")
((and (string=? s "8") (string=? t "6")) "4")
((and (string=? s "8") (string=? t "7")) "5")
((and (string=? s "8") (string=? t "8")) "6")
((and (string=? s "8") (string=? t "9")) "7")
((and (string=? s "9") (string=? t "1")) "0")
((and (string=? s "9") (string=? t "2")) "1")
((and (string=? s "9") (string=? t "3")) "2")
((and (string=? s "9") (string=? t "4")) "3")
((and (string=? s "9") (string=? t "5")) "4")
((and (string=? s "9") (string=? t "6")) "5")
((and (string=? s "9") (string=? t "7")) "6")
((and (string=? s "9") (string=? t "8")) "7")
(else "8"))))
Il programma TABELLINA, come si vede, contribuisce a scrivere
correttamente la prima cifra di una somma, ma nulla dice circa
gli eventuali riporti di una somma. Ad esempio
(TABELLINA "5" "8") e (TABELLINA "1" "2") danno lo stesso risultato,
"3". Per effettuare correttamente le somma,
occorre l'informazione se due cifre sommate generano
o meno un riporto (come accade nelle somme
di "5" e "8", ma non di "1" e "2"). Allo scopo creiamo
un programma dal nome RIPORTO?, che, prese come argomento
due cifre, produrrà come risultato un valore di verità:
true, se le cifre da sommare danno riporto, false altrimenti.
Ad esempio:
(RIPORTO? "5" "8") -> true
(RIPORTO? "1" "2") -> false
(RIPORTO? "0" "9") -> false
(RIPORTO? "3" "7") -> true
Come il programma TABELLINA, anche RIPORTO? è un programma
noioso da scrivere (per quanto semplice),
dovendo nuovamente contemplare le combinazioni possibili
di somme di due cifre.
Siccome noi supponiamo di non disporre né dell'operazione
di somma, né di un test di "minore uguale", che consentirebbe
di abbreviare il programma,
saremo costretti a usare un lungo test, introdotto
da un or iniziale che consentirà di avere risposta
true per tutte le combinazioni (date dagli and interni),
per cui la somma delle due cifre sarà maggiore o uguale a 10.
Ecco il codice di RIPORTO?.
(define RIPORTO?
(lambda (s t)
(or (and (string=? s "9") (string=? t "1"))
(and (string=? s "9") (string=? t "2"))
(and (string=? s "9") (string=? t "3"))
(and (string=? s "9") (string=? t "4"))
(and (string=? s "9") (string=? t "5"))
(and (string=? s "9") (string=? t "6"))
(and (string=? s "9") (string=? t "7"))
(and (string=? s "9") (string=? t "8"))
(and (string=? s "9") (string=? t "9"))
(and (string=? s "8") (string=? t "2"))
(and (string=? s "8") (string=? t "3"))
(and (string=? s "8") (string=? t "4"))
(and (string=? s "8") (string=? t "5"))
(and (string=? s "8") (string=? t "6"))
(and (string=? s "8") (string=? t "7"))
(and (string=? s "8") (string=? t "8"))
(and (string=? s "8") (string=? t "9"))
(and (string=? s "7") (string=? t "3"))
(and (string=? s "7") (string=? t "4"))
(and (string=? s "7") (string=? t "5"))
(and (string=? s "7") (string=? t "6"))
(and (string=? s "7") (string=? t "7"))
(and (string=? s "7") (string=? t "8"))
(and (string=? s "7") (string=? t "9"))
(and (string=? s "6") (string=? t "4"))
(and (string=? s "6") (string=? t "5"))
(and (string=? s "6") (string=? t "6"))
(and (string=? s "6") (string=? t "7"))
(and (string=? s "6") (string=? t "8"))
(and (string=? s "6") (string=? t "9"))
(and (string=? s "5") (string=? t "5"))
(and (string=? s "5") (string=? t "6"))
(and (string=? s "5") (string=? t "7"))
(and (string=? s "5") (string=? t "8"))
(and (string=? s "5") (string=? t "9"))
(and (string=? s "4") (string=? t "6"))
(and (string=? s "4") (string=? t "7"))
(and (string=? s "4") (string=? t "8"))
(and (string=? s "4") (string=? t "9"))
(and (string=? s "3") (string=? t "7"))
(and (string=? s "3") (string=? t "8"))
(and (string=? s "3") (string=? t "9"))
(and (string=? s "2") (string=? t "8"))
(and (string=? s "2") (string=? t "9"))
(and (string=? s "1") (string=? t "9")))))
Possiamo a questo punto entrare nel cuore dell'algoritmo
che consentirà di sommare le rappresentazioni di due numeri
decimali. Cominciamo ad analizzare qualche esempio.
Partiamo da alcuni casi molto semplici, dove, per il momento,
non ci sono casi di riporto. Come sommare ad esempio
"3" e "4"?
Sappiamo che in questo caso il risulato corretto della somma deve essere
"7", ossia vorremo che il programma finale SOMMA lavori nel modo
seguente:
(SOMMA "3" "4") -> "7"
Per il momento osserviamo che
(TABELLINA "3" "4") -> "7"
Proviamo ora un'altra somma: ci aspettiamo
(SOMMA "15" "23") -> "38"
Osserviamo che
(TABELLINA "5" "3") -> "8"
(TABELLINA "1" "2") -> "3"
Ancora un ulteriore esempio: ci aspettiamo
(SOMMA "152" "236") -> "388"
Osserviamo (l'osservazione è ora meno banale):
(TABELLINA "2" "6") -> "8"
(SOMMA "15" "23") -> "38" (vedi esempio precedente)
(SOMMA "152" "236") = (string-append (SOMMA "15" "23") (TABELLINA "2" "6"))
Si osservi che
"2" = (ULTIMO "152")
"6" = (ULTIMO "236")
"15" = (C-ULTIMO "152")
"23" = (C-ULTIMO "236")
Raccogliendo quanto visto sopra arriviamo a questa osservazione,
valida a patto che non ci siano riporti: ogni qualvolta
dobbiamo sommare due rappresentazioni di numeri decimali, s e t,
composte da diverse cifre,
possiamo procedere alla concatenazione:
(string-append (SOMMA (C-ULTIMO s) (C-ULTIMO t)) (TABELLINA (ULTIMO s)
(ULTIMO t)))
Cosa accade se le cifre generano riporti? Osserviamo
il seguente esempio, simile a quello precedente. Dal nostro
programma SOMMA ci aspettiamo:
(SOMMA "152" "239") -> "391"
Si osservi che
(TABELLINA "2" "9") -> "1"
(RIPORTO? "2" "9") -> true
(SOMMA-con-riporto "15" "23") = (SOMMA "16" "23") -> "39"
"16" = il successore di "15"
Come si può osservare, sopra è comparso un nuovo
elemento, la nozione di successore, che consente di effettuare
la somma nei casi in cui ci sia riporto nella somma
delle cifre delle unità. Al momento, nonostante sembri
utile, non disponiamo di un programma SUCC che calcoli la
rappresentazione del successore di un numero n, data la rappresentazione
di n. Il programma SUCC dovrebbe lavorare ad esempio come segue:
(SUCC "13") -> "14"
(SUCC "19") -> "20"
(SUCC "2199") -> "2200"
Al fine di non spezzare il filo del discorso, si supponga di
disporre del programma SUCC (sul cui codice torneremo dopo).
A questo punto, facendo l'ipotesi che le cifre delle unità
di s e t producano un riporto (segnalato dal risultato
true effettuando il test RIPORTO?
su (ULTIMO s) e (ULTIMO t))
potremo applicare la seguente formula per calcolare la somma:
(SOMMA s t) =
(string-append (SOMMA (SUCC (C-ULTIMO s)) (C-ULTIMO t)) (TABELLINA (ULTIMO s)
(ULTIMO t)))
Possiamo a questo punto scrivere il codice di SOMMA.
Al suo interno utilizzeremo, oltre ai programmi
RIPORTO?, TABELLINA, ULTIMO e C-ULTIMO, anche il programma
SUCC che verrà scritto in seguito.
Il programma SOMMA comprende due test iniziali per stabilire
se una delle due stringhe è vuota, nel qual caso
il risultato della somma sarà pari all'altra stringa.
Questo caso deve essere considerato poiché
ha luogo quando si sommano rappresentazioni con numeri
differenti di cifre (se ad esempio si sommano "13" e "4",
dopo la somma delle unità fra "3" e "4", resterà
da sommare "1" con la stringa vuota "", che corrisponde
a (C-ULTIMO "4")). Se entrambe le stringhe hanno almeno
una cifra, si procederà, con i test successivi, a verificare
se le due stringhe s e t hanno cifre delle unità che producono
o meno riporto, e ci si regolerà conseguentemente,
come sopra spiegato, per calcolare la somma complessiva.
(define SOMMA
(lambda (s t)
(cond ((string=? s "") t)
((string=? t "") s)
((RIPORTO? (ULTIMO s) (ULTIMO t))
(string-append
(SOMMA (SUCC (C-ULTIMO s)) (C-ULTIMO t))
(TABELLINA (ULTIMO s) (ULTIMO t))
))
(else
(string-append
(SOMMA (C-ULTIMO s) (C-ULTIMO t))
(TABELLINA (ULTIMO s) (ULTIMO t))
)))))
Resta infine da scrivere il codice del programma SUCC.
Lo riportiamo qua senza commenti. I lettori provino a comprendere
da soli l'algoritmo che è alla base della sua stesura.
(define SUCC
(lambda (s)
(cond ((string=? s "") "1")
((string=? (ULTIMO s) "0") (string-append (C-ULTIMO s) "1"))
((string=? (ULTIMO s) "1") (string-append (C-ULTIMO s) "2"))
((string=? (ULTIMO s) "2") (string-append (C-ULTIMO s) "3"))
((string=? (ULTIMO s) "3") (string-append (C-ULTIMO s) "4"))
((string=? (ULTIMO s) "4") (string-append (C-ULTIMO s) "5"))
((string=? (ULTIMO s) "5") (string-append (C-ULTIMO s) "6"))
((string=? (ULTIMO s) "6") (string-append (C-ULTIMO s) "7"))
((string=? (ULTIMO s) "7") (string-append (C-ULTIMO s) "8"))
((string=? (ULTIMO s) "8") (string-append (C-ULTIMO s) "9"))
(else (string-append (SUCC (C-ULTIMO s)) "0")))))
Esercizi proposti
I prossimi esercizi costituiscono un piccolo
test per coloro che vogliono testare la preparazione
raggiunta. Vanno quindi svolti prima di leggerne la soluzione.
L'ordine di difficoltà è casuale.
Esercizio 8.1: Scrivere un programma
CONTA-a che, data una stringa, conta quante "a" sono presenti in essa.
Sol.
(define CONTA-a
(lambda (s)
(cond ((string=? s "") 0)
((string=? (PRIMO s) "a") (+ 1 (CONTA-a (C-PRIMO s))) )
(else (CONTA-a (C-PRIMO s))) )))
Esercizio 8.2: Scrivere un programma
IPOTENUSA che, dati i lati di un triangolo rettangolo,
calcola l'ipotenusa.
Sol.
(define IPOTENUSA
(lambda (a b)
(sqrt (+ (* a a) (* b b)))))
Esercizio 8.3: Scrivere un programma
DIAGONALE che, dato il lato del quadrato, ne calcola
la diagonale.
Sol.
(define DIAGONALE
(lambda (lato)
(* (sqrt 2) lato)))
Esercizio 8.4: Scrivere un programma
PREFISSO-COMUNE che, date due stringhe s e t, calcola
il prefisso più lungo comune ad entrambe le stringhe. Ad esempio:
(PREFISSO-COMUNE "fumo" "fuoco") → "fu"
(PREFISSO-COMUNE "casa" "base") → ""
(PREFISSO-COMUNE "casa" "cena") → "c"
(PREFISSO-COMUNE "panettiere" "panegirico") → "pane
Sol.
(define PREFISSO-COMUNE
(lambda (s t)
(cond ((or (string=? s "") (string=? t "")) "")
((string=? (PRIMO s) (PRIMO t))
(string-append
(PRIMO s) (PREFISSO-COMUNE (C-PRIMO s) (C-PRIMO t))) )
(else ""))))
...