Home / Indice sezione | www.icosaedro.it | ![]() |
In questo tutorial vedremo alcune delle caratteristiche di M2 attraverso semplici esempi. Daremo per scontata la conoscenza delle nozioni di base della programmazione, e quindi non ci dilungheremo a spiegare cos'è una variabile o cosè una istruzione per il controllo di flusso. Alcuni esempi si possono provare on-line: basta cliccare sull'ancora sotto il sorgente per accedere alla maschera di editing, compilazione ed esecuzione del programma. Naturalmente è anche possibile modificare i programmi proposti.
MODULE HelloWorld (* ** The basic of all the programs from any time ever: hello world! *) IMPORT m2 BEGIN print("Hello, world!\n") END
Questo è il nostro primo sorgente M2. Salviamo questo testo nel
file Hello.mod
. Lo compiliamo ed eseguiamo dando il comando
m2 Hello.mod -r
Se tutto è andato bene, vedremo apparire sullo schermo il famigerato messaggio. Osservando il sorgente, notiamo che:
MODULE
,
BEGIN
e END
sono scritte in lettere maiuscole.
m2
che contiene alcune funzioni
di utilità, tra le quali la print()
.
print()
stampa sullo schermo la stringa
che abbiamo indicato. In questo caso la stringa termina con la sequenza
\n
che produce il carattere di a-capo.
MODULEHello
" non sarebbe valida.
MODULE PythagorasTables (* ** Print the multiplication tables. An example of nested FOR loops. *) IMPORT m2 VAR a, b: INTEGER BEGIN FOR a=1 TO 10 DO print("Table of the " + a + ":\n") FOR b=1 TO 10 DO print( "\t" + a + " * " + b + " = " + (a*b) + "\n" ) END print("\n") END END
Salviamo questo testo nel file Tabelline.mod
. Lo possiamo
compilare ed eseguire come abbiamo fatto per l'esempio 1. In questo
programma notiamo che:
VAR
introduce la dichiarazione delle variabili
a
,b
di tipo INTEGER
. M2 è un
linguaggio fortemente tipizzato: non è possibile mescolare tipi di dato
diversi, salvo che attraverso le apposite funzioni di conversione.
FOR
permettono di iterare le istruzioni mentre
una variabile indice assume tutti i valori tra due estremi, nel nostro
caso da 1 a 10. Quindi i due cicli nidificati permettono di eseguire
tutti i possibili prodotti di due numeri interi compresi tra 1 e 10.
print()
viene generato
concatenando vari spezzoni di stringa con l'operatore di concatenamento
di stringhe +
. I numeri interi vengono automaticamente
trasformati in stringhe prima di essere concatenati.
MODULE LeggiFile IMPORT m2, io VAR fn: STRING (* nome del file *) f: FILE (* file descriptor *) buf: STRING (* i/o buffer *) BEGIN print("File: ") fn = input() # leggi da tastiera il nome del file Open(f, fn, "r") LOOP (* leggi un blocco di 4 KB: *) buf = Read(f, 1024*4) IF buf = NIL THEN EXIT END print(buf) END Close(f) END
In questo esempio notiamo che:
io
che contiene le
funzioni di uso generale per la lettura e la scrittura dei file, come
Open()
, Read()
e Close()
.
(*
... *)
si
possono estendere su più righe e possono anche essere nidificati
tra di loro; invece quelli che iniziano con il carattere #
terminano alla fine della riga.
STRING
: si tratta di stringhe
di lunghezza arbitraria e che possono contenere qualsiasi carattere, anche
il codice ASCII zero. Usiamo la variabile fn
per contenere
il nome del file, e la variabile buf
per contenere il buffer
dei dati letti.
Open(f, fn, "r")
apre in lettura il file
e ritorna il descrittore f
da usare nelle funzioni di i/o
successive. Se la lettura dal file fallisce per un qualsiasi motivo
(ad esempio il file non esiste o non è accessibile in lettura),
la funzione genera una condizione di errore che blocca il programma con
l'emissione di un messaggio di errore.
LOOP
... END
dove ad ogni
iterazione leggiamo un blocco di dati nella variabile buf
.
Read(f, 1024*4)
legge dal file fino a
4 KB e li assegna alla stringa buf
. Quando i dati sono
finiti, la funzione ritorna il valore speciale NIL
.
Usiamo questo valore per uscire dal ciclo con EXIT
.
Close(f)
chiude il file.
LeggiFile(fn)
legge l'intero contenuto di un file di testo di nome fn
e
lo ritorna in un array di stringhe. Ogni elemento dell'array contiene una
riga del file. La funzione Trova(parola, a)
stampa tutte le
stringhe dell'array che contengono la parola indicata.
MODULE Ricerca IMPORT m2, io FUNCTION LeggiFile(fn: STRING): ARRAY OF STRING VAR f: FILE i: INTEGER line: STRING a: ARRAY OF STRING BEGIN Open(f, fn, "r") i = 0 LOOP line = ReadLine(f) IF line = NIL THEN EXIT END a[i] = line i = i+1 END Close(f) RETURN a END FUNCTION Trova(p: STRING, a: ARRAY OF STRING) VAR n, i: INTEGER BEGIN n = 0 FOR i = 0 TO count(a)-1 DO IF match(a[i], p) THEN print(a[i] + "\n") n = n+1 END END print("Trovate " + n + " ricorrenze.\n") END VAR fn: STRING parola: STRING a: ARRAY OF STRING BEGIN print("Inserisci il nome del file: ") fn = input() a = LeggiFile(fn) print("Inserisci la parola da cercare (espressione regolare): ") parola = input() Trova(parola, a) END
In questo esempio notiamo che:
FUNCTION
definisce una funzione,
i suoi argomenti, l'eventuale valore di ritorno, e il codice che la
implementa. La funzione Trova()
non ritorna alcun valore
e pertanto viene detta più propriamente una procedura.
count(a)
ritorna il numero di elementi dell'array. Gli
elementi dell'array sono sempre indicizzati a partire da zero.
match(s, re)
esegue il confronto tra la
stringa s
e l'espressione regolare re
. Nel caso
più banale di espressione regolare, la funzione match()
si limita a verificare se la parola appare nella stringa. Tuttavia
M2 supporta lo strumento più generale delle espressioni regolari che
permette di analizzare ed estrarre informazioni da una stringa.
Anche in questo esempio abbiamo ignorato la gestione degli errori. La compilazione degli esempi precedenti produce parecchi messaggi di warning che ci avvisano che certe funzioni che abbiamo impiegato potrebbero causare condizioni di errore:
Ricerca.mod:8:14: warning: the function `Open' may RAISE ERROR - use TRY...END Ricerca.mod:11:29: warning: the function `ReadLine' may RAISE ERROR - use TRY...END Ricerca.mod:18:15: warning: the function `Close' may RAISE ERROR - use TRY...END
Il compilatore ci avverte che queste funzioni possono generare condizioni
di errore: ignorandole, il nostro programma verrebbe terminato qualora una
di queste condizioni dovesse verificarsi. Per esempio, se tentassimo di
aprire un file inesistente di nome xyz
ecco cosa apparirebbe:
File: xyz io.Open(), line 108: "xyz": No such file or directory (code 2)
Il messaggio fa riferimento alla posizione della funzione nel modulo di
libreria io
. Per la gestione degli errori M2 dispone di
una apposita istruzione TRY...END, ma non ce ne occuperemo qui. Diciamo
solo che, usando questa istruzione, il messaggio di errore sarebbe stato
più circostanziato e avrebbe riportato anche la posizione esatta
nel nostro programma dell'istruzione che ha causato l'errore.
Manipolare stringhe di caratteri in M2 è molto semplice, e si possono
manipolare singoli caratteri o lunghe sequenze con uguale facilità.
L'operatore di sottostringa [i,j]
restituisce l'intervallo
della stringa indicato. L'operatore di sottostringa [i]
restituisce un carattere, ed è semplicemente la forma abbreviata
dell'intervallo [i,i+1]
. La figura qui sotto dovrebbe
chiarire meglio come gli intervalli di caratteri sono determinati.
In questo esempio la funzione Reverse(s)
ritorna la stringa
ottenuta invertendo l'ordine dei caratteri nella stringa passata per
argomento. Avremmo potuto usare un semplice ciclo FOR
,
ma qui per rendere le cose più interessanti lo facciamo usando
la ricorsione. Il meccanismo è semplice: la funzione spezza a
metà la stringa, quindi chiama sè stessa per invertire le
due parti, poi unisce i pezzi scambiandone l'ordine. Mentre le chiamate
ricorsive della funzione proseguono, ogni nuova istanza della funzione
riceve una stringa più breve, fino a quando essa si riduce a un
carattere soltanto o alla stringa vuota: in questi due casi non è
necessario invertire nulla.
MODULE ReverseString (* ** Reverse the characters in a string using a recursive method. ** Demostrate the usage of the substrings and of the recursive ** functions. *) IMPORT m2 CONST MYSTRING = "Programming in M2" FUNCTION Reverse(s: STRING): STRING VAR l: INTEGER BEGIN l = length(s) IF l < 2 THEN RETURN s ELSE RETURN Reverse(s[l DIV 2, l]) + Reverse(s[0, l DIV 2]) END END BEGIN print( MYSTRING + " <=> " + Reverse( MYSTRING ) + "\n" ) END
Da notare che l'operatore di divisione tra numeri interi è "DIV" e non
"/" per sottolineare che il risultato è troncato della parte
decimale: 7 DIV 5 dà 1 e non 1.4. L'operatore MOD dà invece
il resto della stessa divisione: 7 MOD 5 dà infatti 2.
Infine, la funzione length(s)
dà la lunghezza della
stringa.
MODULE BinaryTree (* ** Classical binary tree: insertion, deletion, traversing. ** Illustrate handling of dynamical data structures and ** function recursion. *) IMPORT m2 TYPE Node = RECORD left, right: Node key: STRING END FUNCTION insert(key: STRING, VAR n: Node) BEGIN IF n = NIL THEN n[key] = key ELSIF key < n[key] THEN insert(key, n[left]) ELSIF key > n[key] THEN insert(key, n[right]) ELSE error("insert(): ignoro chiave duplicata `" + key + "'\n") END END FUNCTION delete(key: STRING, VAR n: Node) FUNCTION del(VAR r: Node) VAR t: Node BEGIN IF r[right] = NIL THEN t = r r = r[left] t[left] = n[left] t[right] = n[right] n = t ELSE del(r[right]) END END BEGIN (* Search the key: *) IF n = NIL THEN error("delete(): chiave `" + key + "' non trovata\n") RETURN ELSIF key < n[key] THEN delete(key, n[left]) RETURN ELSIF key > n[key] THEN delete(key, n[right]) RETURN END (* Key found: *) IF n[left] = NIL THEN n = n[right] ELSIF n[right] = NIL THEN n = n[left] ELSE del(n[left]) END END FUNCTION print_in_order(n: Node) BEGIN IF n = NIL THEN RETURN END print_in_order(n[left]) print(n[key] + "\n") print_in_order(n[right]) END FUNCTION print_tree(n: Node, a: STRING, b: STRING) BEGIN print(a) IF n = NIL THEN print("*\n") RETURN END print(n[key] + "\n") print_tree(n[right], b + "|`", b + "| ") print_tree(n[left], b + " `", b + " ") END VAR root: Node BEGIN insert("M", root) insert("O", root) insert("D", root) insert("U", root) insert("L", root) insert("E", root) print("Sorted list:\n") print_in_order(root) print("\nOriginal tree:\n") print_tree(root, NIL, NIL) print("\nNow delete node `E'. Resulting tree:\n") delete("E", root) print_tree(root, NIL, NIL) print("\nNow delete node `D'. Resulting tree:\n") delete("D", root) print_tree(root, NIL, NIL) print("\nNow delete node `M'. Resulting tree:\n") delete("M", root) print_tree(root, NIL, NIL) END
Tutte le parole sono disposte in un albero binario i cui nodi sono di
tipo Node
. Il tipo Node
è un RECORD
che contiene tre campi: il campo key
è una parola,
mentre i campi left
e right
sono i rami sinistro
e destro dell'albero. La variabile globale root
è
la radice dell'albero.
La funzione insert(s, root)
inserisce la
parola s
nell'albero la cui radice è
root
. Quando la funzione aggiunge una nuova parola viene
automaticamente creato un nuovo nodo. I campi del nuovo nodo che
non vengono esplicitamente assegnati assumono il valore default. Nel
nostro caso significa che i campi left
e right
valgono NIL.
Una volta terminato l'inserimento, la funzione
print_in_order(root)
stampa tutte le parole nell'ordine
alfabetico. Infine, la funzione print_tree(root, NIL, NIL)
stampa l'albero binario in "arte ASCII" mettendo degli asterischi in
corrispondenza dei campi NIL.
Supponiamo che le parole che vengono inserite nel programma siano "M", "O", "D", "U", "L", "A". L'output del programma sarà:
In-order list: D E L M O U Tree structure: M |`O | |`U | | |`* | | `* | `* `D |`L | |`* | `E | |`* | `* `*
Riportiamo anche una funzione che esegue la ricerca di una parola nell'albero:
FUNCTION search(key: STRING, n: Node): Node BEGIN IF n = NIL THEN RETURN NIL ELSIF key < n[key] THEN RETURN search(key, n[left]) ELSIF key > n[key] THEN RETURN search(key, n[right]) ELSE RETURN n END END
Richiamata ad esempio con search("D", root)
questa funzione
ritorna il nodo corrispondente. Se la chiave non viene trovata, la funzione
ritorna NIL.
Cancellare un nodo dall'albero è più complicato:
FUNCTION delete(key: STRING, VAR n: Node) FUNCTION del(VAR r: Node) VAR t: Node BEGIN IF r[right] = NIL THEN t = r r = r[left] t[left] = n[left] t[right] = n[right] n = t ELSE del(r[right]) END END BEGIN IF n = NIL THEN error("delete(): key `" + key + "' not found.\n") RETURN ELSIF key < n[key] THEN delete(key, n[left]) RETURN ELSIF key > n[key] THEN delete(key, n[right]) RETURN END IF n[left] = NIL THEN n = n[right] ELSIF n[right] = NIL THEN n = n[left] ELSE del(n[left]) END END
Questa funzione cancella il nodo con chiave key
dall'albero
n
. Ad esempio, la funzione potrebbe essere invocata come
delete("D", root)
. La funzione prima ricerca il nodo da
cancellare e se non lo trova stampa un messaggio di errore. Se il nodo
da cancellare non ha discendenti il problema si risolve facilmente, basta
togliere il nodo. Se il nodo da togliere ha un solo discendente, allora
basta sostituire il nodo da cancellare con il nodo discendente.
Più complicato il caso in cui il nodo da cancellare ha due
discendenti. Si hanno due possibilità: esaminare il sottoalbero
di sinistra del nodo da cancellare e individuare il nodo più a
destra e sostituire questo nodo a quello da cancellare; in alternativa,
esaminare il sottoalbero di destra del nodo da cancellare e individuare
il nodo più a sinistra e sostituire questo nodo a quello da
cancellare. La funzione del()
segue la prima strategia.
Ecco cosa succede cancellando in sequenza le parole "E", "D" e "M":
Situazione iniziale: delete("E", root)
delete("D", root)
delete("M", root)
M |`O | |`U | | |`* | | `* | `* `D |`L | |`* | `E | |`* | `* `* M |`O | |`U | | |`* | | `* | `* `D |`L | |`* | `* `* M |`O | |`U | | |`* | | `* | `* `L |`* `* L |`O | |`U | | |`* | | `* | `* `*
MODULE
e risiedevano in file con estensione
.mod
. La compilazione dei programmi MODULE
produce un programma eseguibile. M2 supporta il concetto di librerie
di moduli: un modulo di libreria è costituito da un DEFINITION
MODULE
, che definisce l'interfaccia, e un IMPLEMENTATION
MODULE
che contiene il codice. I rispettivi file hanno estensione
.def
e .imp
. A titolo di esempio, mostriamo
un semplice modulo per la generazione di numeri casuali. Prima il
DEFINITION MODULE
:
DEFINITION MODULE Random (* Banale implementazione per un generatore di numeri casuali. *) FUNCTION NewSequence(seed: INTEGER) (* genera una nuova sequenza di numeri "casuali" generati a partire da seed *) FUNCTION Rnd(): INTEGER (* Ritorna il prossimo numero "casuale". *) END
La compilazione del DEFINITION MODULE
comporta la semplice
verifica della sua correttezza e non genera alcun file. I moduli
clienti, siano essi dei MODULE
, dei DEFINITION
MODULE
o degli IMPLEMENTATION MODULE
, potranno
importare le costanti, i tipi, le variabili e le funzioni definite in
questo DEFINITION MODULE
(in questo caso esportiamo solo
le due funzioni NewSequence()
e Rnd()
).
Ed ora il modulo di implementazione:
IMPLEMENTATION MODULE Random CONST a = 23 b = 12345 m = 65536 VAR seed: INTEGER FUNCTION NewSequence(s: INTEGER) BEGIN seed = s MOD m END FUNCTION Rnd(): INTEGER BEGIN seed = (a*seed + b) MOD m RETURN seed END END
La compilazione del modulo di implementazione del modulo Random produce
un codice linkabile ai moduli clienti. I moduli clienti, siano essi
dei MODULE
, dei DEFINITION MODULE
o degli
IMPLEMENTATION MODULE
, potranno importare le costanti,
i tipi, le variabili e le funzioni dichiarate nel modulo di definizione
usando la dichiarazione IMPORT Random
.
Osserviamo che, mentre il modulo di definizione resta "in chiaro" (si tratta di un semplice testo), il modulo di implementazione consiste nel sorgente che abbiamo visto e dal quale si ricava il file linkabile. Nel complesso, i file generati saranno tre:
Random.def Random.imp Random.lib
Per utilizzare il modulo Random
il compilatore
necessita del modulo di definizione e del file linkabile; il sorgente
del modulo di implementazione non è necessario.
open()
''. Ad esempio, il modulo io
esporta
la funzione open()
che apre un file nella modalità non
bufferizzata; il modulo win
esporta una funzione con lo
stesso nome che apre una finestra; il modulo pg
apre una
connessione con il DBMS PostgreSQL; infine, il modulo net
esporta una funzione che apre una connessione di rete TCP. Una
applicazione articolata può desiderare di utilizzare parecchi moduli,
sicchè si presenta il problema della collisione dei nomi. M2 risolve
la situazione con il meccanismo della qualificazione dei nomi.
Un nome qualificato è costituito dal nome di un modulo, seguito dal
punto, seguito dal nome dell'oggetto esportato da quel modulo.
Esempio:
MODULE DBClient IMPORT io, win, pg, net VAR f: FILE window: win.Window pg: pg.Connection socket: INTEGER BEGIN io.open(f, "data", "r") window = win.open(display, screen, 0, 0, 200, 150) pg = pg.open("masterdb") socket = net.open("localhost", 110) ... END
L'identificatore FILE
viene esportato solo dal modulo
io
e pertanto lo possiamo usare non qualificato senza
ambiguità.
Invece esistono varie open()
esportate da tanti moduli
diversi: qui la qualificazione si rende necessaria per risolvere
l'ambiguità. Se non avessimo usato la qualificazione il compilatore
avrebbe segnalato il problema e l'elenco dei moduli che esportano
l'identificatore in questione.
Il meccanismo della qualificazione si applica anche alle costanti, alle variabili e ai tipi. La qualificazione è obbligatoria solo in caso di ambiguità. Al contrario, la qualificazione può essere usata anche quando non strettamente necessaria quando ciò possa contribuire a migliorare la comprensibilità del sorgente.
Per provare gli esempi, e per costruire i tuoi programmi, avrai bisogno del compilatore e delle librerie di M2. Vai nella sezione download e segui le istruzioni.
La descrizione completa del linguaggio si trova nel report.
Per sviluppare programmi più articolati avrai bisogno delle librerie di M2. La sezione library elenca tutte le librerie disponibili disponibili nel pacchetto di installazione.
Nella sezione applications trovi alcune applicazioni realizzate in M2.
Umberto Salsi | Commenti | Contatto | Mappa | Home / Indice sezione |
Still no comments to this page. Use the Comments link above to add your contribute.