[>a2233.html>] [<a2231.html<] [^a2.html^]
Il linguaggio Scheme ha una filosofia che si basa fondamentalmente sul suo tipo di notazione. Scheme è un linguaggio utile per rappresentare un problema, più che per realizzare un programma completo. La standardizzazione di questo linguaggio è riferita fondamentalmente a un documento che viene aggiornato periodicamente: RnRS, ovvero Revised-n Report on the Algorithmic Language Scheme, dove n è il numero di questa revisione (attualmente dovrebbe trattarsi della quinta). Tuttavia, la standardizzazione riguarda gli aspetti fondamentali del linguaggio, mentre ogni realizzazione che utilizza Scheme introduce le estensioni necessarie alle circostanze.
In questo capitolo si vogliono descrivere solo alcuni degli aspetti più importanti di questo linguaggio. Il documento di riferimento è quello citato, ovvero R5RS; alla fine del capitolo si possono trovare anche altri riferimenti per guide più o meno dettagliate su Scheme.
Il linguaggio Scheme prevede dei commenti, che vengono ignorati regolarmente: si distinguono perché iniziano con un punto e virgola (;) e terminano alla fine della riga. Generalmente, le righe vuote e quelle bianche sono ignorate nello stesso modo. In generale, le istruzioni Scheme hanno l'aspetto di qualcosa che è racchiuso tra parentesi tonde.
(display "Ciao")
Per comprenderne il senso, l'esempio precedente potrebbe essere espresso come si vede sotto, se lo si volesse rappresentare in un linguaggio ipotetico, basato sulle funzioni:
display ("Ciao")
Tutto quello che si fa con Scheme viene ottenuto attraverso chiamate di funzione, ovvero, secondo la terminologia utilizzata da R5RS, procedure, che possono restituire o meno un valore. Le chiamate di queste procedure, o di queste funzioni, iniziano con un nome, posto subito dopo la parentesi tonda di apertura, continuando eventualmente con l'elenco dei parametri che gli vengono passati, separati semplicemente da uno o più spazi, anche verticali (non si utilizzano virgole o altri simboli di interpunzione), terminando con la parentesi tonda di chiusura.
(nome [parametro_1 [parametro_2]... [parametro_n]])
Da quanto affermato, si intende anche che un'istruzione può essere interrotta in qualunque punto in cui potrebbe essere inserito uno spazio, per riprenderla nella riga successiva, incolonnandola in base allo stile preferito. Si osservi l'esempio seguente:
(+ 3 4)
si tratta di una chiamata a una funzione denominata +, a cui vengono passati i parametri 5 e 4. Si intende, intuitivamente, che questa funzione restituisca la somma dei parametri.
Le istruzioni non hanno bisogno di essere terminate da un qualche simbolo di interpunzione, dal momento che le parentesi tonde esprimono chiaramente l'estensione di queste e l'ambito relativo all'interno dei vari annidamenti. |
Questo tipo di notazione ha diversi pregi, ma ha il difetto fondamentale di essere un po' difficile da seguire visivamente, soprattutto a causa dell'affollarsi delle parentesi tonde.
In questi capitoli si cercherà di utilizzare un allineamento di queste parentesi che renda più facile la lettura delle istruzioni, anche se si tratta di uno stile che di solito non si applica. |
Per facilitare la comprensione degli esempi, in questi capitoli dedicati a Scheme, si utilizzerà il simbolo ===> per indicare il valore restituito da una funzione (che appare alla sua destra). |
I nomi utilizzati per «identificare» qualunque cosa in Scheme, possono essere scritti utilizzando le lettere dell'alfabeto, le cifre numeriche, e una serie di caratteri particolari che vengono considerati come un'estensione ai caratteri alfabetici:
! $ % & * + - . / : < = > ? @ ^ _ ~
Non tutte le combinazioni sono possibili: in generale non è ammissibile che tali nomi inizino con una cifra numerica.
In generale, Scheme non dovrebbe fare differenza tra lettere maiuscole e minuscole nei nomi che identificano qualcosa. |
È importante osservare che, a differenza di altri linguaggi di programmazione, caratteri come +, -, * e /, possono essere (e in pratica sono) dei nomi. Come è già stato fatto osservare,
(+ 3 4)
è la chiamata della funzione (procedura) +, a cui vengono passati i valori tre e quattro come parametri.
Anche se si possono usare caratteri insoliti nei nomi degli identificatori, quando si dichiara qualcosa, come il nome di una variabile, o di una funzione, è bene astenersi dalle cose troppo stravaganti, a meno che ci sia un buon motivo per le scelte che si fanno. In generale, sono già stabilite delle convenzioni per i nomi delle funzioni, almeno quelle che fanno già parte del linguaggio standard:
le funzioni il cui nome termina con un punto interrogativo (?) sono intese essere dei «predicati», ovvero delle funzioni che verificano l'avverarsi di una condizione (la verità di un'affermazione), e restituiscono un valore booleano;
le funzioni il cui scopo è quello di modificare il valore di una variabile, senza cambiarne l'allocazione (per la precisione si tratta di modificare un valore in un'area di memoria già allocata), terminano con un punto esclamativo (!);
Le funzioni il cui scopo è quello di convertire un «oggetto» di un tipo, in un altro di tipo differente, contengono un -> all'interno del nome.
Per permettere di comprendere meglio come possa essere formato un identificatore, si osservi l'elenco seguente di nomi, che rappresentano tutti delle possibilità valide:
ciao a b + - * list->vector ABCdef123 A123b124 <=? ciao-come-stai-io-sto-bene-grazie
Scheme è un linguaggio basato sulle funzioni, per quanto queste vengano chiamate «procedure» nella sua terminologia specifica. Questo significa, per esempio, che tutte le espressioni che si possono scrivere con Scheme sono dei valori costanti, oppure delle chiamate di funzione, più o meno annidate. Anche le strutture di controllo sono realizzate in forma di funzione.
È importante osservare che in Scheme non esiste una funzione principale che debba essere eseguita prima delle altre; si segue semplicemente l'ordine sequenziale in cui appaiono le istruzioni. In generale, con lo stesso criterio, le funzioni che si utilizzano devono essere state dichiarate prima del loro utilizzo.
Scheme utilizza una gestione speciale per le variabili. La dichiarazione di una variabile implica l'allocazione di uno spazio di memoria adatto e l'abbinamento del puntatore relativo a una variabile.
(define variabile [valore_iniziale])
Per esempio,
(define x 1)
alloca l'area di memoria necessaria a contenere un numero intero, quindi abbina all'identificatore x il puntatore a questa area. In pratica, l'identificatore x si comporta come una variabile di un linguaggio di programmazione «normale», dal momento che quando viene valutato in un'espressione restituisce esattamente il valore a cui punta.
Questa distinzione, non è soltanto una questione di pignoleria, ma si tratta di un punto fondamentale della filosofia di Scheme: la dichiarazione successiva dello stesso identificatore, non va a modificare l'informazione precedente, ma alloca una nuova area di memoria. L'allocazione precedente non viene recuperata e potrebbe continuare a essere utilizzata da ciò che è stato dichiarato prima del cambiamento. In questo senso, a livello teorico, il linguaggio Scheme non prevede un sistema di eliminazione degli oggetti inutilizzati (lo spazzino, ovvero il garbage collector), benché le realizzazioni possano attuare in pratica queste forme di ottimizzazione quando sono in grado di provare che un'area di memoria allocata non può più essere presa in considerazione nel programma.
Proprio a causa di questa particolarità di Scheme, per assegnare un valore a un'area di memoria già allocata, attraverso l'identificatore relativo, si utilizza la funzione set!:
(set! variabile espressione_del_valore_da_assegnare)
Il punto esclamativo finale che compone il nome della funzione, serve a sottolineare il fatto che si ottiene la modifica di un valore già allocato, senza allocare un'altra area di memoria.
I dati secondo Scheme sono organizzati in oggetti, ma non nel senso che viene attribuito dai linguaggi di programmazione a oggetti (object oriented). I tipi di dati di Scheme sono precisamente:
booleano -- inteso come il risultato di un'espressione logica, o una costante booleana;
coppia (lista non vuota);
simbolico -- che fa riferimento a costanti simili alle stringhe, ma che sono trattate diversamente in Scheme;
numerico;
carattere -- un carattere singolo che non è una stringa;
stringa;
porta, o flusso -- ovvero un file aperto;
I dati hanno una loro essenza e una loro rappresentazione esterna, che corrisponde al modo in cui vengono espressi a livello umano. Questa rappresentazione può consentire a volte l'uso di forme diverse ed equivalenti; per esempio, il numero 16 può essere espresso con la sequenza dei caratteri 16, oppure #d16, #x10, e in altri modi ancora.
Tuttavia, è bene osservare che un oggetto per Scheme può essere di un tipo solo. Si parla in questo senso di «tipi disgiunti». |
Scheme fornisce alcuni predicati, ovvero alcune funzioni, per il controllo del tipo a cui appartiene un oggetto. Nello stesso ordine in cui sono stati elencati i tipi di dati, si tratta di: boolean?, pair?, symbol?, number?, char?, string?, vector?, port?, procedure?. Per esempio, l'istruzione seguente restituisce Vero se l'identificatore x fa riferimento a un numero:
(number? x)
Tra tutti i tipi di dati visti, ne esiste uno speciale: la lista vuota, che non appartiene alle coppie. Per identificare una lista di qualunque tipo, includendo anche quelle vuote, si usa il predicato list?.
Predicato | Descrizione |
(boolean? espressione) | Vero se l'espressione dà come risultato un valore logico booleano. |
(pair? espressione) | Vero se l'espressione dà come risultato una «coppia» (lista non vuota). |
(list? espressione) | Vero se l'espressione dà come risultato una lista (anche vuota). |
(symbol? espressione) | Vero se l'espressione dà come risultato un simbolo. |
(number? espressione) | Vero se l'espressione dà un risultato numerico di qualunque tipo. |
(char? espressione) | Vero se l'espressione dà come risultato un carattere. |
(string? espressione) | Vero se l'espressione dà come risultato una stringa. |
(vector? espressione) | Vero se l'espressione dà come risultato un vettore. |
(port? espressione) | Vero se l'espressione dà come risultato una «porta». |
(procedure? espressione) | Vero se l'espressione dà come risultato una funzione. |
Tabella 180.1. Elenco dei predicati utili per verificare l'appartenenza ai vari tipi di dati.
Scheme ha una gestione particolare delle espressioni, dove al loro interno è speciale la gestione dei valori costanti. Questo fatto verrà chiarito nel seguito. Tuttavia, è necessario conoscere subito in che modo possono essere indicati i valori più comuni in un sorgente Scheme.
I valori booleani possono essere rappresentati attraverso la sigla #t per Vero e #f per Falso.
I valori numerici possono essere usati nel modo consueto, quando si tratta di valori interi (positivi o negativi), quando si vogliono indicare numeri che hanno un numero fisso di decimali, e quando si usa la notazione scientifica comune (xey).
67 +67 -67 678.67 +678.67 -678.67 6.7867e2 67867e-3
In aggiunta a quello che si può vedere dagli esempi mostrati sopra, si possono indicare dei valori specificando la base di numerazione. Per ottenere questo, si utilizza un prefisso del tipo
#x
dove x è una lettera che esprime la base di numerazione. Segue l'elenco di questi prefissi:
#b -- numero binario;
#o -- numero ottale;
#d -- numero decimale;
#x -- numero esadecimale.
Per esempio, #x10 è equivalente a #d16, ovvero a 16 senza prefissi.
Scheme consente di utilizzare anche altri tipi di notazioni, per indicare alcuni tipi particolari di numeri. Questa caratteristica di Scheme viene descritta più avanti.
Scheme ha una gestione speciale delle espressioni costanti, cosa che verrà descritta in seguito. Ugualmente, è prevista la presenza delle stringhe, rappresentate attraverso una sequenza di caratteri delimitata da una coppia di apici doppi: "...".
All'interno delle stringhe è previsto l'uso di sequenze di escape composte dalla barra obliqua inversa (\) seguita da un carattere. Secondo lo standard R5RS è prevista solo la sequenza \", per inserire un apice doppio, e \\, per poter inserire una barra obliqua inversa. Le varie realizzazioni di Scheme, possono prevedere l'utilizzazione di altre sequenze di escape, per esempio come avviene nel linguaggio C.
(display "ciao a tutti, sì, proprio a \"tutti\"") (newline)
In Scheme, i caratteri sono qualcosa di diverso dalle stringhe, ma questo vale anche per altri linguaggi di programmazione. Tuttavia, la rappresentazione di una costante carattere è molto diversa rispetto alle stringhe:
#\carattere | #\nome_carattere
Questi caratteri, sempre secondo Scheme, sono oggetti singoli e non possono essere uniti assieme a formare una stringa, a meno di utilizzare delle funzioni apposite di conversione in stringa. Segue un elenco che mostra alcuni esempi di rappresentazione di questi oggetti carattere.
#\a -- la lettera «a» minuscola;
#\A -- la lettera «A» maiuscola;
#\( -- la parentesi tonda aperta;
#\ -- lo spazio (dopo la barra obliqua inversa c'è esattamente un carattere <SP>;
#\space -- lo spazio, espresso per nome;
Un'espressione è qualcosa che, per mezzo di una valutazione, fa qualcosa, oppure restituisce un qualche valore, o fa tutte e due le cose. Le espressioni sono cose che riguardano praticamente tutti i linguaggi di programmazione, ma Scheme ha una gestione particolare quando si vuole evitare che qualcosa venga trasformato da una valutazione.
In pratica, in Scheme si distinguono le espressioni letterali, che sono delle espressioni che per qualche ragione, non devono essere elaborate nel modo consueto, ma passate così come sono in modo letterale.
Nella filosofia di Scheme non si hanno delle variabili vere e proprie, ma degli identificatori che fanno riferimento a delle zone di memoria allocate. Tuttavia, si può usare ugualmente il termine «variabile», se si fa attenzione a ricordare la particolarità di Scheme.
La valutazione di una variabile in Scheme genera la restituzione del valore contenuto nell'area di memoria a cui questa punta. Se si usa un interprete Scheme, come quelli descritti nel capitolo introduttivo di questa parte, si può osservare questo fatto in modo molto semplice:
(define x 195) x ===> 195
In pratica, l'espressione banale che consiste nell'indicare semplicemente l'identificatore di una variabile, genera la restituzione del valore che in precedenza gli è stato assegnato.
In un linguaggio di programmazione qualunque, le espressioni letterali corrispondono alle costanti letterali, come i numeri, le stringhe e oggetti simili. In Scheme si aggiungono anche altri oggetti.
costante
'dato
(quote dato)
A parte le costanti letterali normali, le altre espressioni letterali si distinguono per essere precedute da un apostrofo iniziale ('), oppure (ed è la stessa cosa), per essere indicate come argomento della funzione quote.
Inizialmente è difficile comprendere il senso di questa notazione. Tuttavia, è importante riconoscere subito che non si tratta di stringhe, in quanto lo scopo per il quale esistono queste espressioni letterali, è proprio quello di evitare che vengano valutate prima del necessario. Si osservino gli esempi seguenti, divisi su tre colonne, allo scopo di facilitarne il confronto. In particolare, si suppone che esista una variabile a che faccia riferimento a una zona di memoria contenente il valore uno.
(quote a) ===> a «simbolo» 'a ===> a «simbolo» a ===> 1 (quote (+ 1 2)) ===> (+ 1 2) '(+ 1 2) ===> (+ 1 2) (+ 1 2) ===> 3 (quote (quote a)) ===> (quote a) ''a ===> (quote a) 'a ===> a «simbolo» (quote "a") ===> "a" «stringa» '"a" ===> "a" «stringa» "a" ===> "a" «stringa» (quote 1) ===> 1 '1 ===> 1 1 ===> 1 (quote #t) ===> #t '#t ===> #t #t ===> #t (quote #\a) ===> #\a «carattere» '#\a ===> #\a «carattere» #\a ===> #\a «carattere»
Nei primi esempi si fa riferimento a qualcosa che si identifica attraverso la lettera «a». (quote a), ovvero 'a, non è un carattere e non è una stringa: è un simbolo non meglio identificato; dipende dal programmatore il significato che questo può avere. Per semplificare le cose, si è immaginato che si trattasse di una variabile.
Tra gli esempi si vede la possibilità di indicare una funzione per la somma, (+ 1 2), come espressione costante. Ci sono situazioni in cui questo è necessario, per esempio quando una funzione deve essere passata come argomento di un'altra, mentre lo scopo non è quello di passare il risultato della valutazione della prima.
Le costanti letterali, come le stringhe, i numeri, i caratteri e i valori booleani, possono essere indicati come espressioni letterali; in tal modo il risultato non cambia, dal momento che la valutazione di tali costanti restituisce le costanti stesse.
Ci sono altri tipi di dati che possono essere indicati in forma di espressioni letterali, ma non sono stati mostrati gli esempi relativi perché questi tipi non sono ancora stati descritti. Tuttavia, il senso non cambia: si usano le espressioni letterali quando non si può lasciare che queste siano valutate.
L'ordine in cui viene valutata un'espressione è relativamente semplice in Scheme, dal momento che non si utilizzano operatori simbolici e tutto è espresso in forma di funzioni. In generale, si valuta prima ciò che sta nella posizione più «interna», venendo mano a mano verso l'esterno. Per esempio,
(* 3 (+ 2 4))
si risolve secondo la sequenza di operazioni elencate di seguito:
3 ===> 3
valutazione di (+ 2 4)
2 ===> 2
4 ===> 4
2+4 ===> 6
3*6 ===> 18
Nei linguaggi di programmazione comuni, le espressioni si avvalgono prevalentemente di operatori di vario tipo, tanto che gli operatori sono di per sé delle funzioni, più o meno celate. Con Scheme, questa ambiguità viene eliminata, dal momento che tutte le operazioni di un'espressione si svolgono per mezzo di funzioni. Le funzioni che vengono descritte in queste sezioni, sono quelle che vengono utilizzate più frequentemente nelle espressioni di Scheme.
Il valore restituito da una funzione può essere di tipo diverso a seconda degli operandi utilizzati. Di solito si fa l'esempio della somma di due interi che genera un risultato intero. Scheme ha una gestione particolare dei numeri, almeno a livello teorico, per cui questi vengono classificati in modo molto più sofisticato di quanto facciano i linguaggi di programmazione normali.
Con Scheme, i numeri sono gestiti a due livelli differenti: l'astrazione matematica e la realizzazione pratica. Dal punto di vista dell'astrazione matematica, si distinguono i livelli seguenti:
numero generico;
numero complesso;
numero reale;
numero razionale;
numero intero.
In generale, un numero che appartiene a una classe inferiore, è anche un numero che può essere considerato appartenente a tutti i livelli superiori. Per esempio, un numero razionale è anche un numero reale, ed è anche un numero complesso, ecc.
Scheme fornisce una serie di predicati (funzioni), per la verifica dell'appartenenza di un valore a un tipo di numero. L'elenco si vede nella tabella 180.2. In generale, queste funzioni restituiscono il valore Vero (#t) nel caso in cui sia valida l'appartenenza presunta.
Predicato | Descrizione |
(number? espressione) | Vero se l'espressione dà un risultato numerico di qualunque tipo. |
(complex? espressione) | Vero se l'espressione dà come risultato un numero complesso. |
(real? espressione) | Vero se l'espressione dà come risultato un numero reale. |
(rational? espressione) | Vero se l'espressione dà come risultato un numero razionale. |
(integer? espressione) | Vero se l'espressione dà come risultato un numero intero. |
Tabella 180.2. Elenco dei predicati utili per verificare l'appartenenza ai vari tipi numerici.
Nel modo in cui si rappresenta un numero si indica implicitamente il tipo di questo. Tuttavia, se Scheme è in grado di conoscere una semplificazione nel modo di rappresentarne il valore, lo classifica automaticamente nella fascia inferiore relativa. Per esempio, se 4/2 viene mostrato come numero razionale, dal momento che è equivalente a due, è anche un intero puro e semplice. Gli esempi seguenti mostrano in che modo possono reagire i predicati per la verifica del tipo numerico. Si osservi in particolare la disponibilità della notazione m/n, che permette di indicare agevolmente i numeri razionali.
(integer? 3) ===> #t (rational? 3) ===> #t (real? 3) ===> #t (complex? 3) ===> #t (number? 3) ===> #t (integer? 6/2) ===> #t (integer? 3/2) ===> #f (rational? 6/2) ===> #t (rational? 3/2) ===> #t (integer? 1.1) ===> #f (rational? 1.1) ===> #t (dipende dalla realizzazione di Scheme) (real? 1.1) ===> #t
Secondo Scheme, i numeri sono esatti o inesatti, a seconda di varie circostanze, che possono dipendere anche dalla realizzazione che si utilizza. In generale, un numero è esatto se è stato fornito attraverso una costante che di per sé è esatta (come un numero intero o un numero razionale), oppure se deriva da numeri esatti utilizzati in operazioni esatte. Si comprende intuitivamente che nel momento in cui si introducono approssimazioni di qualche tipo, per qualche ragione, i valori che si ottengono dai calcoli che si fanno, non sono precisi, ma sono, appunto, inesatti. Nonostante sia molto facile generare risultati inesatti, anche quando si parte da valori esatti, ci sono alcune situazioni in cui i risultati sono esatti anche se i valori di partenza sono inesatti; per esempio, la moltiplicazione per uno zero esatto, genera uno zero esatto, qualunque sia l'altro valore. A proposito dell'esattezza o meno dei valori, sono disponibili alcune funzioni che sono elencate nella tabella 180.3.
Funzione | Descrizione |
(exact? espressione) | Vero se l'espressione dà un risultato numerico esatto. |
(inexact? espressione) | Vero se l'espressione dà un risultato numerico inesatto. |
(exact->inexact espressione) | Converte il risultato dell'espressione in un valore numerico inesatto. |
(inexact->exact espressione) | Converte il risultato dell'espressione in un valore numerico esatto. |
Tabella 180.3. Elenco dei predicati e delle altre funzioni riferite ai valori esatti e inesatti.
Seguono alcuni esempi sull'uso di queste funzioni.
(exact? 3) ===> #t (exact? 3/2) ===> #t (exact? 1.5) ===> #f (exact->inexact 3) ===> 3.0 (inexact->exact 1.5) ===> 3/2
Come accennato all'inizio, oltre all'astrazione matematica si pone il problema della precisione dei valori inesatti (quelli che per altri linguaggi di programmazione sono semplicemente dei valori a virgola mobile). Ammesso che la realizzazione di Scheme permetta di distinguere tra diversi livelli di precisione, si possono rappresentare delle costanti numeriche «reali» (a virgola mobile), utilizzando la notazione esponenziale, dove al posto della lettera «e» consueta, si utilizzano rispettivamente le lettere, s, f, d e l, che indicano valori a precisione ridotta (short), a singola precisione (float), a doppia precisione (double) e a precisione ancora maggiore (long).
Funzione | Descrizione |
(+ op...) | Somma gli argomenti. |
(* op...) | Moltiplica gli argomenti. |
(- op) | Moltiplica il valore dell'operando per -1. |
(- op1 op2...) | Sottrae dal primo la somma degli operandi successivi. |
(/ op) | Divide il primo operando per 1. |
(/ op1 op2...) | Divide il primo operando per il secondo, divide il risultato per il terzo... |
(log op) | Calcola il logaritmo naturale. |
(exp op) | Calcola l'esponente. |
(sin op) | Calcola il seno. |
(cos op) | Calcola il coseno. |
(tan op) | Calcola la tangente. |
(asin op) | Calcola l'arco-seno. |
(acos op) | Calcola l'arco-coseno. |
(atan op) | Calcola l'arco-tangente. |
(sqrt op) | Calcola la radice quadrata. |
(expt op1 op2) | Eleva il primo operando alla potenza del secondo. |
(abs op) | Calcola il valore assoluto. |
(quotient op1 op2) | Divide il primo operando per il secondo e restituisce il valore intero. |
(remainder op1 op2) | Resto della divisione del primo operando per il secondo. |
(modulo op1 op2) | Calcola il modulo (vedere nota). |
(ceiling op) | Calcola la parte intera per eccesso. |
(floor op) | Calcola la parte intera per difetto. |
(round op) | Calcola la parte intera più vicina. |
(truncate op) | Calcola la parte intera eliminando semplicemente la parte decimale. |
(max op...) | Restituisce il valore massimo dei suoi operandi. |
(min op...) | Restituisce il valore minimo dei suoi operandi. |
(gcd n_intero...) | Calcola il massimo comune divisore dei vari operandi. |
(lcd n_intero...) | Calcola il minimo comune multiplo dei vari operandi. |
(numerator n_razionale) | Restituisce il numeratore di un numero razionale. |
(denomiator n_razionale) | Restituisce il denominatore di un numero razionale. |
Tabella 180.4. Elenco delle funzioni matematiche comuni.
La tabella 180.4 riporta l'elenco delle funzioni più comuni che possono essere usate nelle espressioni aritmetiche e matematiche. In particolare si deve osservare che remainder e modulo si comportano nello stesso modo, tranne quando si utilizzano valori negativi (per approfondire la differenza si può leggere il documento di riferimento su Scheme, ovvero R5RS).
Scheme permette di utilizzare più di due operandi per le funzioni che sommano, sottraggono, dividono e moltiplicano. A parte la spiegazione sintetica data nella tabella in cui sono state presentate, si può intendere il senso del loro funzionamento immaginando che le operazioni avvengono in modo progressivo, da sinistra a destra:
(- 5 3 2)
equivale a:
(- (- 5 3) 2)
Nello stesso modo,
(/ 5 3 2)
equivale a:
(/ (/ 5 3) 2)
Infine, la tabella 180.5 riporta alcuni predicati utili per classificare in qualche modo un valore numerico.
Funzione | Descrizione |
(zero? op) | Vero se l'operando equivale a zero. |
(positive? op) | Vero se l'operando è un numero positivo. |
(negative? op) | Vero se l'operando è un numero negativo. |
(odd? op) | Vero se l'operando è un numero dispari. |
(even? op) | Vero se l'operando è un numero pari. |
Tabella 180.5. Elenco di altri predicati utili per classificare i valori numerici.
Sono già state presentate le costanti booleane #t e #f, che valgono per Vero e Falso rispettivamente. Per Scheme, da un punto di vista logico-booleano, valgono come Vero anche le liste (che verranno descritte in seguito), compresa la lista vuota, i simboli, i numeri, le stringhe, i vettori e le funzioni. In pratica, qualsiasi oggetto diverso dal tipo booleano, assieme al valore booleano #t, vale come Vero, mentre solo #f vale per Falso. Tuttavia, per verificare che un oggetto corrisponda effettivamente a un valore booleano, si può usare il predicato
(boolean? oggetto)
che restituisce Vero in caso affermativo.
Alcune realizzazioni più vecchie di Scheme trattano la lista vuota, che si rappresenta con (), come equivalente al valore booleano Falso. |
Gli operatori logici sono realizzati in Scheme attraverso funzioni. La tabella 180.6 elenca queste funzioni.
Funzione | Descrizione |
(not op) | Inverte il risultato logico dell'operando. |
(and op1 op2...) | Vero se tutti gli operandi restituiscono Vero. |
(or op1 op2...) | Vero se anche solo un operando restituisce Vero. |
Tabella 180.6. Elenco delle funzioni logiche.
Per quanto riguarda il confronto, si distinguono situazioni diverse, a seconda che si vogliano confrontare dei valori numerici, carattere, stringa, oppure che si vogliano confrontare gli «oggetti». Le tabelle 180.7, 180.8, 180.9, e 180.10, riepilogano le funzioni in grado di eseguire tali confronti.
Funzione | Descrizione |
(= op1 op2...) | Vero se gli operandi si equivalgono. |
(< op1 op2...) | Vero se gli operandi sono in ordine crescente. |
(> op1 op2...) | Vero se gli operandi sono in ordine decrescente. |
(<= op1 op2...) | Vero se gli operandi sono in ordine non decrescente. |
(>= op1 op2...) | Vero se gli operandi sono in ordine non crescente. |
Tabella 180.7. Elenco delle funzioni per il confronto numerico.
È interessante notare che le funzioni per il confronto ammettono l'uso di più di due argomenti. Si osservino gli esempi seguenti, con i risultati che restituiscono.
(= 2 2) ===> #t (= 2 2 2) ===> #t (= 2 2 2 1) ===> #f (< 1 2) ===> #t (< 1 2 3) ===> #t (< 1 2 3 2) ===> #f
Funzione | Descrizione |
(char=? car1 car2) | Vero se i due caratteri sono uguali. |
(char<? car1 car2) | Vero se il primo carattere è lessicograficamente inferiore al secondo. |
(char>? car1 car2) | Vero se il primo carattere è lessicograficamente superiore al secondo. |
(char<=? car1 car2) | Vero se il primo carattere è lessicograficamente non superiore al secondo. |
(char>=? car1 car2) | Vero se il primo carattere è lessicograficamente non inferiore al secondo. |
(char-ci=? car1 car2) | Come char=?, senza distinguere tra maiuscole e minuscole. |
(char-ci<? car1 car2) | Come char<?, senza distinguere tra maiuscole e minuscole. |
(char-ci>? car1 car2) | Come char>?, senza distinguere tra maiuscole e minuscole. |
(char-ci<=? car1 car2) | Come char<=?, senza distinguere tra maiuscole e minuscole. |
(char-ci>=? car1 car2) | Come char>=?, senza distinguere tra maiuscole e minuscole. |
Tabella 180.8. Elenco delle funzioni per il confronto tra caratteri.
Per quanto riguarda il confronto tra caratteri e tra stringhe, non è stabilita la possibilità di inserire più di due argomenti, anche se è possibile che una realizzazione Scheme lo consenta.
(char<? #\a #\b) ===> #t (char<? #\A #\B) ===> #t (char-ci<=? #\A #\b) ===> #t (char-ci<=? #\a #\B) ===> #t (char-ci=? #\a #\A) ===> #t
Funzione | Descrizione |
(string=? str1 str2) | Vero se le due stringhe sono uguali. |
(string<? str1 str2) | Vero se la prima stringa è lessicograficamente inferiore alla seconda. |
(string>? str1 str2) | Vero se la prima stringa è lessicograficamente superiore alla seconda. |
(string<=? str1 str2) | Vero se la prima stringa è lessicograficamente non superiore alla seconda. |
(string>=? str1 str2) | Vero se la prima stringa è lessicograficamente non inferiore alla seconda. |
(string-ci=? str1 str2) | Come string=?, senza distinguere tra maiuscole e minuscole. |
(string-ci<? str1 str2) | Come string<?, senza distinguere tra maiuscole e minuscole. |
(string-ci>? str1 str2) | Come string>?, senza distinguere tra maiuscole e minuscole. |
(string-ci<=? str1 str2) | Come string<=?, senza distinguere tra maiuscole e minuscole. |
(string-ci>=? str1 str2) | Come string>=?, senza distinguere tra maiuscole e minuscole. |
Tabella 180.9. Elenco delle funzioni per il confronto tra stringhe.
(string<? "ab" "aba") ===> #t (string<? "AB" "ABA") ===> #t (string-ci<? "AB" "aba") ===> #t (string-ci<? "ab" "ABA") ===> #t (string-ci=? "ciao" "CIAO") ===> #t
Scheme offre dei predicati particolari per il confronto tra due oggetti, come mostrato nella tabella 180.10. È difficile definire in modo chiaro la differenza che c'è tra questi tre predicati. In generale si può affermare che equal? sia il predicato che è più permissivo, mentre eq? è quello più restrittivo.
Funzione | Descrizione |
(eq? op1 op2) | Vero se i due operandi sono identici. |
(eqv? op1 op2) | Vero se i due operandi sono equivalenti dal punto di vista operativo. |
(equal? op1 op2) | Vero se i due operandi hanno la stessa struttura e lo stesso contenuto. |
Tabella 180.10. Elenco delle funzioni per il confronto tra gli oggetti.
(equal? "abc" "abc") ===> #t (eqv? "abc" "abc") ===> #f (eq? "abc" "abc") ===> #f (equal? 2 2) ===> #t (eqv? 2 2) ===> #t (eq? 2 2) ===> (non specificato) (equal? 'a 'a) ===> #t (eqv? 'a 'a) ===> #t (eq? 'a 'a) ===> #t
Alcune funzioni specifiche per i caratteri sono elencate nella tabella 180.11. Per quanto riguarda il caso particolare del predicato char-whitespace?, questo si avvera nel caso in cui si tratti di <SP>, <HT>, <LF>, <FF> e <CR>.
Funzione | Descrizione |
(char? oggetto) | Vero se l'oggetto è un carattere. |
(char-alphabetic? carattere) | Vero se il carattere è alfabetico. |
(char-numeric? carattere) | Vero se il carattere è numerico. |
(char-whitespace? carattere) | Vero se si tratta di uno spazio orizzontale o verticale. |
(char-upper-case? carattere) | Vero se si tratta di un carattere alfabetico maiuscolo. |
(char-lower-case? carattere) | Vero se si tratta di un carattere alfabetico minuscolo. |
(char->integer carattere) | Restituisce un numero corrispondente al carattere. |
(integer->char numero_intero) | Restituisce un carattere corrispondente al numero. |
(char-upcase carattere) | Se possibile, converte il carattere in maiuscolo. |
(char-downcase carattere) | Se possibile, converte il carattere in minuscolo. |
Tabella 180.11. Elenco di alcune funzioni specifiche per la gestione dei caratteri.
Nella conversione attraverso le funzioni char->integer e integer->char, l'equivalenza tra carattere e numero dipende dalla realizzazione di Scheme; molto probabilmente dipenderà dalla codifica dell'insieme di caratteri utilizzato.
Alcune funzioni specifiche per i caratteri sono elencate nella tabella 180.12. Quando le funzioni fanno riferimento a un indice per indicare un carattere all'interno di una stringa, si deve ricordare che il primo corrisponde alla posizione zero. Quando si fa riferimento a due indici, uno per indicare il carattere iniziale e uno per fare riferimento al carattere finale, il secondo indice deve puntare alla posizione successiva all'ultimo carattere da prendere in considerazione. Questo permette di individuare una stringa nulla quando l'indice iniziale e l'indice finale sono uguali.
Funzione | Descrizione |
(string? oggetto) | Vero se l'oggetto è una stringa. |
(make-string numero_caratteri) | Restituisce una stringa della lunghezza indicata. |
(make-string numero_caratteri carattere) | Restituisce una stringa composta con il carattere indicato. |
(string carattere...) | Restituisce una stringa composta dai caratteri indicati. |
(string-length stringa) | Restituisce il numero di caratteri contenuto. |
(string-ref stringa indice) | Restituisce il carattere nella posizione dell'indice. |
(string-set! stringa indice carattere) | Modifica il carattere che si trova nella posizione dell'indice. |
(substring stringa inizio fine) | Estrae la sottostringa compresa tra i due indici. |
(string-append stringa...) | Restituisce una stringa unica complessiva. |
(string-copy stringa) | Restituisce una copia della stringa. |
(string-fill! stringa carattere) | Sostituisce gli elementi della stringa con il carattere indicato. |
(string->list stringa) | Restituisce una lista composta dai caratteri della stringa. |
(list->string lista_di_caratteri) | Restituisce una stringa a partire da una lista di caratteri. |
Tabella 180.12. Elenco di alcune funzioni specifiche per la gestione delle stringhe.
(make-string 10 #\A) ===> "AAAAAAAAAA" (string-length "ciao") ===> 4 (define a "ciao") (string-set! a 0 #\C) a ===> "Ciao" (substring a 2 4) ===> "ao"
Anche con Scheme sono disponibili le strutture di controllo comuni nei linguaggi di programmazione. Evidentemente, queste sono realizzate attraverso delle funzioni. In base a tale impostazione, per sottoporre una parte di codice alla verifica di una condizione, o per metterla in un ciclo, occorre che questa sia inserita in una funzione che possa essere chiamata all'interno di un'espressione.
Per intendere il problema, si osservi l'esempio seguente, che mostra la scelta tra la chiamata della funzione display per visualizzare il messaggio «bello», o «brutto», in funzione di una condizione (che in questo caso si avvera necessariamente):
(if (> 3 2) (display "bello") (display "brutto"))
Per ovviare a questo inconveniente si può utilizzare la funzione begin, che permette di incorporare più espressioni dove invece se ne potrebbe inserire una sola.
Per tutte le situazioni in cui è possibile indicare una sola espressione, mentre invece se ne vorrebbero inserire diverse, esiste la funzione begin:
(begin espressione espressione ... )
Il senso si comprende intuitivamente: le espressioni che costituiscono gli argomenti di begin vengono valutati in ordine, da sinistra a destra (in questo caso dall'alto in basso). L'esempio seguente è molto banale: visualizza un messaggio e termina.
(begin (display "ciao ") (display "a ") (display "tutti!") (newline) )
È importante osservare che all'interno della funzione begin non è possibile dichiarare delle variabili locali, a meno che per questo si inseriscano delle altre funzioni che creano un loro ambiente, come let e le altre simili. |
La struttura condizionale è il sistema di controllo fondamentale dell'andamento del flusso delle istruzioni.
(if condizione espressione_se_vero [espressione_se_falso])
La funzione if valuta i suoi argomenti in un ordine preciso: per prima cosa viene valutato il primo argomento; se il risultato è Vero, o comunque se si ottiene un risultato equiparabile a Vero, valuta il secondo argomento; in alternativa, valuta il terzo argomento, se è stato fornito. Alla fine restituisce il valore dell'ultima espressione a essere stata valutata (ammesso che questa restituisca qualcosa). Sotto vengono mostrati alcuni esempi in cui alcune parti del programma sono state saltate per non distrarre l'attenzione del lettore.
(define Importo 0) ... (if (> Importo 10000000) (display "L'offerta è vantaggiosa"))
---------
(define Importo 0) ... (if (> Importo 10000000) (display "L'offerta è vantaggiosa") (display "Lascia perdere") )
---------
(define Importo 0) ... (if (> Importo 10000000) (display "L'offerta è vantaggiosa") (if (> Importo 5000000) (display "L'offerta è accettabile") (display "Lascia perdere") ) )
Come accennato, potrebbe essere conveniente l'utilizzo della funzione begin per facilitare la descrizione di gruppi di istruzioni (espressioni). Si osservi l'esempio seguente, in cui viene salvato il valore dell'importo nella variabile Offerta:
(define Importo 0) (define Offerta 0) ... (if (> Importo 10000000) ; then (begin (display "L'offerta è vantaggiosa") (set! Offerta Importo) ; eventualmente fa anche qualcosa in più ;... ) ; else (if (> Importo 5000000) ; then (begin (display "L'offerta è accettabile") (set! Offerta Importo) ; eventualmente fa anche qualcosa in più ;... ) ; else (display "Lascia perdere") ; end if ) ; end if )
Scheme fornisce due strutture di selezione. In questo caso, la funzione cond si basa sulla verifica di condizioni distinte per ogni blocco di espressioni.
(cond (condizione espressione...) (condizione espressione...) ... [(else espressione...)] )
Lo schema sintattico dovrebbe essere chiaro a sufficienza: la funzione cond ha come argomenti una serie di «blocchi» (si tratta di liste, ma questo verrà chiarito quando verranno mostrate le liste), contenenti ognuno un'espressione iniziale che deve essere valutata per determinare se le espressioni successive devono essere valutate o meno. Nel momento in cui si incontra una condizione che si avvera, i blocchi successivi vengono ignorati. Se non si incontra alcuna condizione che si avvera, se esiste l'ultimo blocco, corrispondente alla funzione else, le espressioni relative vengono eseguite.
A differenza della funzione if, in questo caso si possono indicare più espressioni per ogni condizione della selezione; in questo senso, la funzione cond può diventare un sostituto opportuno di quella. Segue un esempio tipico di selezione.
(define Mese 0) ... (cond ((= Mese 1) (display "gennaio") (newline)) ((= Mese 2) (display "febbraio") (newline)) ((= Mese 3) (display "marzo") (newline)) ((= Mese 4) (display "aprile") (newline)) ((= Mese 5) (display "maggio") (newline)) ((= Mese 6) (display "giugno") (newline)) ((= Mese 7) (display "luglio") (newline)) ((= Mese 8) (display "agosto") (newline)) ((= Mese 9) (display "settembre") (newline)) ((= Mese 10) (display "ottobre") (newline)) ((= Mese 11) (display "novembre") (newline)) ((= Mese 12) (display "dicembre") (newline)) (else (display "mese errato!") (newline)) )
Scheme fornisce anche la struttura di selezione tradizionale, ovvero la funzione case, che si basa sulla verifica del valore di una sola «chiave». Anche case permette l'indicazione di più espressioni per ogni elemento della selezione.
(case espressione_di_selezione ((dato...) espressione...) ((dato...) espressione...) ... [(else espressione...)] )
La prima espressione a essere valutata è quella che costituisce il primo argomento della funzione case. Successivamente, il suo risultato viene comparato con quello dei «dati» elencati all'inizio di ogni gruppo di espressioni (si vedano gli esempi). Se la comparazione ha successo, allora vengono valutate le espressioni successive (all'interno del blocco), nell'ordine in cui si trovano. Se il confronto non ha successo, se esiste un blocco finale costituito dalla funzione else, vengono eseguite le espressioni relative. Seguono alcuni esempi.
(define Mese 0) ... (case Mese ((1) (display "gennaio") (newline)) ((2) (display "febbraio") (newline)) ((3) (display "marzo") (newline)) ((4) (display "aprile") (newline)) ((5) (display "maggio") (newline)) ((6) (display "giugno") (newline)) ((7) (display "luglio") (newline)) ((8) (display "agosto") (newline)) ((9) (display "settembre") (newline)) ((10) (display "ottobre") (newline)) ((11) (display "novembre") (newline)) ((12) (display "dicembre") (newline)) (else (display "mese errato!") (newline)) )
---------
(define Anno 0) (define Mese 0) (define Giorni 0) ... (case Mese ((1 3 5 7 8 10 12) (set! Giorni 31)) ((4 6 9 11) (set! Giorni 30)) ((2) (if (or (and (= (modulo Anno 4) 0) (not (= (modulo Anno 100) 0))) (= (modulo Anno 400) 0) ) (set! Giorni 29) (set! Giorni 28) ) ) )
Scheme dispone di una funzione unica per realizzare i cicli iterativi e quelli enumerativi. Si tratta di do, il cui funzionamento è, a prima vista, un po' strano. Come ciclo iterativo la sintassi si riduce al modello seguente:
(do () (condizione_di_uscita [espressione_pre_uscita...]) espressione_del_ciclo... )
In questa forma, viene valutata prima la condizione; se si avvera, vengono valutate le espressioni successive, quelle contenute nello spazio delle parentesi (la lista della condizione), quindi il ciclo termina. Se la condizione non si avvera, vengono eseguite le espressioni esterne al blocco della condizione, al termine delle quali riprende il ciclo.
Quando si vuole usare la funzione do per realizzare un ciclo enumerativo, si definiscono una o più variabili da inizializzare e modificare in qualche modo a ogni ciclo:
(do ((variabile inizializzazione passo)...) (condizione_di_uscita [espressione_pre_uscita...]) espressione_del_ciclo... )
Le variabili vengono dichiarate (allocate) dalla funzione do stessa, avendo effetto solo in ambito locale, all'interno della funzione che le dichiara (in pratica, mascherano temporaneamente altre variabili esterne con lo stesso nome). Le variabili vengono inizializzate immediatamente con il valore ottenuto dall'espressione di inizializzazione, quindi inizia il primo ciclo. Alla fine di ogni ciclo, prima dell'inizio del successivo, vengono valutate le espressioni del passo, assegnando alle variabili relative i valori che si ottengono.
L'esempio seguente fa apparire per 10 volte la lettera «x». Si osservi l'uso di una variabile esterna per scandire i cicli.
(define Contatore 0) (do () ((>= Contatore 10)) ; incrementa il contatore di un'unità (set! Contatore (+ Contatore 1)) (display "x") ) (newline)
La stessa cosa avrebbe potuto essere ottenuta dichiarando la variabile all'interno della funzione do:
(do ((Contatore 0 Contatore)) ; condizione di uscita ((>= Contatore 10)) ; incrementa il contatore di un'unità (set! Contatore (+ Contatore 1)) (display "x") ) (newline)
Infine, si può trasferire l'incremento del contatore nel blocco in cui si dichiara e si inizializza la variabile Contatore.
(do ((Contatore 0 (+ Contatore 1))) ; condizione di uscita ((>= Contatore 10)) ; istruzioni del ciclo (display "x") ) (newline)
Un programma Scheme termina quando si esauriscono le istruzioni, oppure quando viene incontrata e valutata la funzione exit.
(exit [valore_di_uscita])
Come si vede dallo schema sintattico, è possibile indicare un numero che si traduce poi nel valore di uscita del programma stesso.
L'utilizzo di questa funzione all'interno di un ambiente di interpretazione Scheme, serve normalmente a concludere il funzionamento del programma relativo.
A. Aaby, Scheme Tutorial, 1996
Pierre Castéran, Robert Cori, Passeport pour Scheme
Il documento citato sembra essere scomparso dalla rete, probabilmente in vista di una sua pubblicazione. In origine, si trovava presso http://dept-info.labri.u-bordeaux.fr/~cori/Bouquins/scheme.ps
.
R5RS -- Revised-5 Report on the Algorithmic Language Scheme, 1998
<http://www.swiss.ai.mit.edu/~jaffer/r5rs_toc.html>
<http://www.swiss.ai.mit.edu/ftpdir/scheme-reports/r5rs.ps.gz>
Schemers.org
1) Nella sezione dedicata ai numeri, è assente la spiegazione riguardo al tipo numerico «complesso». Questo dipende dalla mancanza di preparazione dell'autore al riguardo. Eventualmente si può consultare il documento R5RS in cui questo argomento è affrontato.