[inizio] [indice generale] [precedente] [successivo] [indice analitico] [contributi]

147. Scheme: introduzione

Il linguaggio Scheme ha una filosofia che si basa fondamentalmente sul tipo di notazione che si utilizza. 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, e alla fine del capitolo si possono trovare anche altri riferimenti per guide più o meno dettagliate su Scheme.

147.1 Aspetto generale

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, e continuano 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 questo capitolo, e in quelli successivi, 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).

147.1.1 Identificatori e convenzioni nei nomi

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 3 e 4 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:

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

147.1.2 Funzioni o procedure

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.

147.2 Allocazione dei dati, espressioni, costanti, oggetti

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, e 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.

147.2.1 Oggetti, tipi di dati e rappresentazione esterna

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:

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 147.1: Elenco dei predicati utili per verificare l'appartenenza ai vari tipi di dati.

147.2.2 Costanti letterali

Scheme ha una gestione particolare delle espressioni, e 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.

147.2.2.1 Costanti booleane

I valori booleani possono essere rappresentati attraverso la sigla #t per Vero e #f per Falso.

147.2.2.2 Costanti numeriche

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 (x e y).

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:

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.

147.2.2.3 Stringhe

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.

Potrebbe venire spontaneo l'utilizzo della sequenza \n per inserire il codice di interruzione di riga all'interno di una stringa; tuttavia, anche se potrebbe funzionare, Scheme prevede la funzione newline, che non prevede l'uso di parametri, e si occupa di fare ciò che serve per ottenere un avanzamento all'inizio della riga successiva.

(display "ciao a tutti, sì, proprio a \"tutti\"")
(newline)

147.2.2.4 Costanti carattere

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.

147.2.3 Espressioni

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.

147.2.3.1 Riferimenti variabili

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.

147.2.3.2 Espressioni letterali

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 1.

(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, e 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, e 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, e 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.

147.2.3.3 Ordine nella valutazione di un'espressione

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:

147.3 Funzioni comuni nelle espressioni e particolarità di alcuni tipi di dati elementari

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. *1*

147.3.1 Numeri

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:

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 147.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 147.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 2, è 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 147.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 147.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 147.4: Elenco delle funzioni matematiche comuni.

La tabella 147.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 147.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 147.5: Elenco di altri predicati utili per classificare i valori numerici.

Scheme dispone di altre risorse per la gestione dei valori numerici, e anche ciò che è stato presentato qui è stato descritto in modo approssimativo. Se si vogliono sfruttare bene tali possibilità di questo linguaggio, è indispensabile studiare bene il documento R5RS, già citato più volte, del quale si trova un riferimento alla fine del capitolo.

147.3.2 Valori logici, funzioni di confronto e funzioni logiche

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, e 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 147.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 147.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 147.7, 147.8, 147.9, e 147.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 147.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 147.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 147.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 147.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 147.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

147.3.3 Caratteri

Alcune funzioni specifiche per i caratteri sono elencate nella tabella 147.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 147.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, e molto probabilmente dipenderà dalla codifica dell'insieme di caratteri utilizzato.

147.3.4 Stringhe

Alcune funzioni specifiche per i caratteri sono elencate nella tabella 147.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 147.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"

147.4 Strutture di controllo

Anche con Scheme sono disponibili le strutture di controllo comuni nei linguaggi di programmazione. Evidentemente, queste sono realizzate attraverso delle funzioni. Data questa 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.

147.4.1 begin

Per tutte le situazioni in cui è possibile indicare una sola espressione, e 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.

147.4.2 if

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, e 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
)

147.4.3 cond

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, e 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))
)

147.4.4 case

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)
	)
    )
)

147.4.5 do

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), e il ciclo termina. Se la condizione non si avvera, vengono eseguite le espressioni esterne al blocco della condizione, e alla fine, il ciclo riprende.

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, e hanno 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, e quindi inizia il primo ciclo. Alla fine di ogni ciclo, prima dell'inizio del successivo, vengono valutate le espressioni del passo, e i valori che si ottengono vengono assegnati alle variabili relative.

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)

147.5 Conclusione di un programma Scheme

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.

147.6 Riferimenti

---------------------------

Appunti Linux 1999.09.21 --- Copyright © 1997-1999 Daniele Giacomini --  daniele @ pluto.linux.it


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.


[inizio] [indice generale] [precedente] [successivo] [indice analitico] [contributi]