| Home / Indice sezione | www.icosaedro.it | ![]() |
M2 - TutorialIn 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 | Contatto | Mappa | Home / Indice sezione |