Appunti di Linguaggi e traduttori

 

per collaborazioni, commenti, critiche, e altro contattateci alla e-mail: clubinfo@libero.it risponderemo al più presto!

Abstract

Questo corso, estratto dalle lezioni di Linguaggi e traduttori tenute dal prof. F. Sorbello alla facoltà di ingegneria Informatica di Palermo, vuole essere una introduzione alla teoria che sta alla base dei linguaggi per calcolatori da quelli più antichi fino a quelli moderni. Il corso si può suddividere in tre parti: Tipi e strutture dei linguaggi, Astrazione e programmazione in grande, Proprietà sintattiche. Per semplicità abbiamo suddiviso ulteriormente il corso in sezioni più piccole da poter visionare direttamente tramite il browser.

Contents

Parte VIII: Programmazione in grande

Termine coniato da De Remer e Kron per indicare le tecniche di programmazione per la realizzazione di programmi molto grandi, che una sola persona non potrebbe fare mai. Dal punto di vista del linguaggio questo si risolve nello spezzare il programma in moduli. Si chiama modulo una qualunque parte di programma che possiede un nome e può essere implementata come una entità più o meno indipendente. Il modulo deve essere quanto più indipendente dal resto del programma, così che i programmatori possano lavorare da soli.
La chiave di volta della modularietà è l'astrazione.<7p>

Un modulo può essere una procedura o una funzione; nel caso più generale un modulo è un gruppo di componenti: tipi, costanti, variabili, procedure, funzioni ... Un modulo incapsula i suoi componenti, alcuni dei quali sono interni (hidden) altri visibili all'esterno (exported).
I concetti che supportano la modularietà sono: package, tipi astratti, oggetti e moduli parametrizzati.

Package

E' un gruppo di componenti dichiarati. Può essere visto come un insieme impacchettato di legami.

Il linguaggio Ada, che è il più moderno, questi concetti li ha sviluppati, è ha inventato, come implemetazione una struttura che si chiama package, che non coincide con il package di cui stiamo parlando, perchè con la stessa struttura riesce a implementare tutti e quattro i meccanismi.


package aaaa is
	c: constant Float := 33.e-8
	g: constant Float := 6.4e-1
	h: constant Float := 6.6e+6
end aaaa

Viene creato un legame fra aaaa e le costanti ivi definite. L'accesso avviene con la stessa struttura dei record del Pascal (pur essendo molto più versatile): aaaa.c, aaaa.g, aaaa.h.


package terra is
	type continente is
		(Africa, Antartide, Asia, Australia, Europa, Nordamerica, Sudamerica);
	raggio: constant Float := 6.e6
	area: constant array (continente) of float := (30.e9, 13.e9, 43.e9, 7.e9, 10.e9, 24.e9, 17.e9);
	popolazione: array (continente) of integer;
end terra

Il pacchetto costruito consente di essere usato, per esempio, nella maniera seguente:


for cont in terra.continente loop
	put (terra.popolazione(cont)/terra.area(cont));
end loop

In generale in Ada i pakage hanno la forma:


package I is
D
end I;

In linguaggio ML la struttura equivalente è la structure:


structure I =
struct
D
end

I package ddel linguaggio Ada distinguono due parti: le componenti interne che fanno parte del corpo, e le componenti esterne che si trovano nella parte delle dichiarazioni:


// parte esterna
package trig is
	function sin (x: float) return float;
	function cos (x: float) return float;
end trig;

// parte interna
package body trig is
	pi: constant float := 3.1416;
	function norm (x: float) return float is ...;
	function sin (x: float) return float is ...;
	function cos (x: float) return float is ...;
end trig;

Si notano chiaramente le parti nascoste, per esempio la funzione norma e le parti esportabili. Una volta definito il package (corpo e definizioni), quello che viene fornito agli altri utenti è la parte delle definizioni: il corpo rimane nascosto, affinchè non si ci debba preoccupare di come funziona.

Ad esempio si può usare la chiamata: trig.cos (theta/2.0).

Il linguaggio ML consente le dichiarazioni relative ai blocchi. E' quindi possibile ottenere informazioni nascoste. Si ricorda che la forma è la seguente:


local D1 in D2 end.

Esempio:


structure trig =
	struct
		local
			val pi = 3.1416;
			fun norm(x: real) = ...
		in
			fun sin (x: real) = ...
			and cos (x: real) = ...
		end
	end

Tipi astratti

Si possono avere notevoli difficoltà nel definire un nuovo tipo, perchè non è facile definire in maniera sintetica qualunque tipo (le liste ad esempio). Per esempio nessun linguaggio possiede gli strumenti per definire il tipo frazione che matematicamente si può definire come:

{rapp (m,n) m,n integer; n>0; m e n non devono avere fattori in comune}

D'altra parte se un linguaggio consentisse tale possibilità esso sarebbe troppo complicato e dovrebbe consentire un controllo dinamico. D'altra parte se la definizione non è precisa e se ne adopera una che si avvicina, si possono incontrare le seguenti difficoltà:

  1. la rappresentazione può assumere valori che non corrispondono ad alcun valore del tipo desiderato: come si fa a controllare che il risultato di operazioni sui tipi non dia luogo a qualcosa che non appartiene più al tipo? Esempio 4/0 non appartiene più al tipo.
  2. la rappresentazione può contenere più valori che corrispondono ad uno stesso valore del tipo desiderato; devo o non devo sviluppare un meccanismo di semplificazzione in maniera che 8/4, 4/2, ½ siano lo stesso valore?
  3. i valori del tipo desiderato e del tipo utilizzato non sarebbero distinguibili come appartenenti a tipi diversi; inoltre nelle espressioni valori come 2/6 deve essere considerato uguale a 1/3?

Il tipo astratto è ottenuto come un modulo, che esternamente sembra un tipo, ma che in realtà non lo è. In Ada possono essere costruiti utilizzzando package con informazioni nascoste:


abstype rational = rat of (int * int)
with
	val zero = rat (0,1)
	and one = rat (1,1);
	
	fun op // (n: int, n: int) =
		if n <>0
		then rat (m,n)
		else ... (* non valido *)
	
	fun op ++ (rat(m1,n1): rational,
		rat (m2,n2): rational =
		rat (m1*n2+m2*n1,n1*n2)
	
	and op == (rat(m1,n1): rational,
		rat (m2,n2): rational =
		(m1*n2 = m2*n1)
end

Oltre al tipo razionale, vengono definiti anche gli operatori che mi consentono di lavorare: // che crea un razionale, ++ che somma due razionali e == che assegna il valore di un razionale ad un'altro.
Una impostazione di questo tipo consente di utilizzare correttamente istruzioni del tipo seguente:


val h = 1//2
...
if one ++ h == 6//4 then ... else ...

All'utente è nascosta la struttura interna rat (a,b). La costruzione di tipi astratti richiede due meccanismi per entrare nel tipo astratto (un codificatore: //, zero, one) e uno che mi consenta di uscirne (un decodificatore: nell'esempio non è presente, ma potrebbe essere svolta dalla funzione

fun float (rat(m,n): rational) = m/n

che restituisce il rapporto tra numeratore e denominatore). In fortran, il tipo complesso possedeva il codificatore (a,b) e il decodificatore (la norma e la funzione atan2).
Si noti ancora che esempi analoghi sarebbero possibili anche per i booleani per i quali è possibile organizzare una struttura nascosta che può, per esempio, utilizzare i numeri interi 1 e 0 per rappresentare vero e falso.

Oggetti

Un'altro tipo di moduli è l'oggetto, che consiste di una variabile (semplice o complessa) nascosta, insieme con un gruppo di operazioni esterne su quella variabile. La variabile è tipicamente una struttura dati e può essere acceduta solo attraverso operazioni esterne. Questo consente, ove occorra, di modificare la struttura dati, senza modificare le modalità di accesso (ovvero l'interfaccia).
Esempio:


package rubrica_object is
	procedure inserisci (nomenuovo : in Name; numeronuovo : in Number);
	procedure esamina (nomeold : in Name; numerold : out Number; trovato : out Boolean);
end rubrica_object;

package body rubrica_object is
	type dirnode;
	type dirpunt is access dirnode;
	type dirnode is record
		entryname : Name;
		entrynumber : Number;
		left, right : dirpunt;
	end record
	root: dirptr;
	
	procedure inserisci (nomenuovo : in Name; numeronuovo : in Number) is ... ;
	procedure esamina (nomeold : in Name; numerold : out Number; trovato : out Boolean) is ...;
begin
...;
end rubrica_object;

In questo caso la variabile è una rubrica (complicata quanto si vuolo) con le proceddure per gestirla. Tutti gli utenti di questo oggetto, possono soltanto usare le due procedure inserisci e esamina, senza preoccuparsi di come sia fatto l'oggetto all'interno.

L'elaborazione di questo package crea un singolo oggetto chiamato rubrica_object che può essere acceduto mediante:


rubrica_object.inserisci (DIE,5957335);
...
rubrica_object.esamina (DIE, numero, ok);

Il compilatore impedisce l'accesso diretto alla variabile root. Si noti che nei compilatori, molti oggetti, per le situazioni più comuni, sono già forniti; quindi anche se non siamo nel caso della programmazzione in grande, già si nota che da un lato c'è un'esperto che costruisce gli oggetti, e dall'altro ci sono gli utenti, a più alto livello, che li utilizzano.

Classi di oggetti

E' una estensione dei concetti prima descritti. Nel linguaggio Ada una classe di oggetti può essere facilmente descritta utilizzando il generic package:


generic package rubrica_object is
	procedure inserisci (nomenuovo : in Name; numeronuovo : in Number);
	procedure esamina (nomeold : in Name; numerold : out Number; trovato : out Boolean);
end rubrica_object;

package body rubrica_object is
	type dirnode;
	type dirpunt is access dirnode;
	type dirnode is record
		entryname : Name;
		entrynumber : Number;
		left, right : dirpunt;
	end record
	root: dirptr;
	
	procedure inserisci (nomenuovo : in Name; numeronuovo : in Number) is ... ;
	procedure esamina (nomeold : in Name; numerold : out Number; trovato : out Boolean) is ...;
begin
...;
end rubrica_object;

Sia la parte dichiarativa che il body sono identici al caso precedente, ma adesso stiamo definendo le caratteristiche della classe, non abbiamo ancora creato nessun oggetto reale. Per farlo bisogna istanziare il generico package:


package rubricacasa is new rubrica_class;
package rubricalavoro is new rubrica_class;

si ottengono due oggetti, che come struttura sono identitici, ma sono entità diverse. Le due strutture si possono utilizzzare così:


rubricacasa.inserisci (DIE 5567890);
rubricalavoro.inserisci (DIE 5567890);

Rispetto al caso precedente ho un tipo di astrazione maggiore, perchè esiste una struttura di alto livello e da questa si può derivare tutte le strutture (oggetti) che si vuole.
Nell'uso pratico gli oggetti che sono necessari sono in numero limitato e la maggior parte è fornito nei pacchetti software. In genere gli oggetti forniti, sono più ampli di quello che è necessario (cioè ci sono funzionalità che magari non si useranno mai), tuttavia spesso è comodo usare questi, piuttosto che andare a scriversi l'oggetto specifico.

I tipi astratti e le classi di oggetti sono simili. Nei tipi astratti si ha un tipo (e quindi le entità appartenenti a quel tipo) con gli operatori; con gli oggetti si una variabile, con gli operatori. Ciascuno di loro ci consente di creare diverse variabili appartenente a tipi la cui rappresentazione è nascosta, e di accedere a queste variabili mediante operatori predisposti allo scopo.
I due concetti sono tuttavia leggermente differenti. L'esempio esporta un tipo astratto di tipo direttorio ed è simile al precedente:


package rubrica_type is
	type directory is limited private
	procedure inserisci (dir: in out directory; nomenuovo : in Name; numeronuovo : in Number);
	procedure esamina (dir : in directory; nomeold : in Name; numerold : out Number; trovato : out Boolean);

private
	type dirnode;
	type directory is access dirnode;
	type dirnode is record
		entryname : Name;
		entrynumber : Number;
		left, right : dirpunt;
	end record
end rubrica_type;

package body rubrica_type is
	procedure inserisci (dir: in out directory; nomenuovo : in Name; numeronuovo : in Number);
	procedure esamina (dir : in directory; nomeold : in Name; numerold : out Number; trovato :
end rubrica_type;

Nella parte privata si hanno delle informazioni del tipo astratto direttorio. Il corpo del package è sempre lo stesso. L'elaborazione di questo package non crea alcuna nuova variabile. Serve semplicemente a legare rubrica_type al seguente set di relazioni:

Tuttavia il tipo astratto consente successivamente di dichiarare diverse variabili, ciascuna delle quali rappresenta un direttorio distinto:


use directory_type;
	rubricacasa : directory;
	rubricalavoro : directory;

...

inserisci (rubricacasa, DIE, 489856);

I tipi astratti sono leggermente più vantaggiosi degli oggetti, perchè sono valori di prima classe, cioè possono essere passati come argomenti nelle procedure, sono più naturali nell'uso, sono utili in tutti i paradigmi dei linguaggi (mentre gli oggetti vanno bene soltanto nei linguaggi imperativi).

Moduli parametrizzati (astrazione generica)

Gli oggetti sono delle entità rigide. Con i moduli parametrizzati, è possibile prendere la classe di oggetti e rendere alcune parti variabili e specificarle successivamente (in base al contesto).
Abbiamo già visto la definizione di astrazione generica. Una astrazione generica è una astrazione su una dichiarazione ed una istanziazione generica che è una dichiarazione che produce legami utilizzando il corpo della stessa.
In Ada il generic package consente di ottenere quanto sopra. La istanziazione


package rubricacasa is new rubrica_class;

è una dichiarazione ed è elaborata nel modo seguente:
vengono elaborate sia le dichiarazioni che il corpo della rubrica_class ed i legami sono incapsulati in un package. I generic package possono essere parametrizzati.
Esempio in Ada:


generic
	capacity : in positive;
package queue_class is
	procedure append (newitem : in Character);
	procedure remove (olditem : out Character);
end queue_class;

package body queue_class is
	items : array (1..capacity) of Character;
	size, front, rear: integer range 0..capacity;
	procedure append (newitem : in Character) is ...;
	procedure remove (olditem : out Character) is ...;
begin
...;
end queue_class;

Il parametro formale di questo package è capacity. E' possibile scrivere una istruzione del tipo:

package line_buffer is new queue_class (129);

Questo 120 è un numero che viene letteralmente passato alla variabile capacity, quindi viene creato un oggetto che ha una struttura fissa, ma al posto di capacity, nella sua definizione ha il valore corrispondente (nell'esempio viene creato una coda di lunghezza 120). Il valore di capacity non è noto in fase di compilazione o di stesura: è noto solo quando si incontra la dichiarazione con la specificazione del parametro. Quindi il parametro potrebbe essere una espressione più o meno complessa.
La dichiarazione è elaborata nella maniera seguente: dapprima il parametro formale capacity è legato all'argomento 120. Successivamente sono elaborate le dichiarazioni ed il corpo per produrre una coda con 120 caratteri. Ancora line_buffer viene usato per denotare il package.

Tipi parametro

Una dichiarazione può fare uso di valori precedentemente definiti, che possono anche essere utilizzati come parametri. E' possibile utilizzare come parametri anche i tipi:


generic
	capacity : in positive;
	type item is private;
package queue_class is
	procedure append (newitem : in item);
	procedure remove (olditem : out item);
end queue_class;

package body queue_class is
	items : array (1..capacity) of item;
	size, front, rear: integer range 0..capacity;
	procedure append (newitem : in item) is ...;
	procedure remove (olditem : out item) is ...;
begin
...;
end queue_class;

E la dichiarazione diventa:


package line_buffer is new queue_class (120, character);

Nell'esempio i parametri formali sono:

Nell'esempio precedente l'istruzione:

type item is private

è prevista dal linguaggio Ada e consente di scrivere assegnazioni:

items (rear) := newitem

dove ambedue i membri dell'espressione sono del tipo item.

La generica istanziazione è elaborata in questo modo:

Questa soluzione, potrebbe essere una cosa non banale da realizzare in pratica, perché fino in fase di esecuzione queste strutture sono lasciate in sospeso, a dipendere dai parametri di cui non si può (in alcuni casi) prevedere ill valore. I tipi parametro denotano un argomento che è sconosciuto: il compilatore deve mettere a punto una struttura versatile, e capace (quando si sa come vano a finire le cose) di adeguarsi alla richiesta. Nel caso specifico, un struttura pronta a costruire un'array, ma non si sa quanto grande, e che cosa deve contenere. Nella prima fase di programmazione, niente è stabilito: la memoria non viene allocata, non si costruiscono legami etc...
Ma niente di utile può essere fatto con i tipi parametro a meno che qualcosa non sia conosciuto, in particolare quali operazioni sono applicabili.

In generale, quindi, l'espressione per avere un tipo parametro T è:

type T is ...

Questa versalità, ha dei risvolti negativi:
se al posto di item si può mettere qualunque tipo a disposizione, se ho previsto delle operazioni che lavorano sulla struttura, tali operazioni devono essere pertinenti con qualunque tipo che io ci metto. Se ad esempio all'interno del body c'è una somma tra due elementi item, deve avere sempre senso questa somma, anche quando si tratta di array, liste, grafi, etc...
Quindi il compilatore verifica che le operazioni applicabili all'argomento siano comprese nell'insieme applicabile a T. Inoltre verifica che le operazioni specificate come applicabili a T siano comprese fra quelle usate per T nell'astrazione generica.
Le ultime affermazioni garantiscono che ogni operazione usata per T sia correttamente applicabile.

I parametri formali sono mutuamente dipendenti. E' possibile cioè per un parametro formale dipendere da un altro parametro e così via. Cioè si può ,costruire un meccanismo di parametrizzazione a cascata, dove un tipo dipende da un altro tipo, etc... Occorre evitare qualunque controllo in fase di esecuzione, poiché in tal caso le prestazioni del calcolatore diminuiscono in maniera considerevole.

Tipi di sistema

Quando si entra in questa ottica, della programmazione in grande, il problema principale che nasce è la difficoltà della progettazione, ovvero la difficoltà nell'individuare la struttura dati. Quando si costruisce un tipo complesso, è necessario andare ad individuare la struttura e gli operatori, che devono lavorarci. Per chi ha già programmato in linguaggi Pascal-like, ha notevoli difficoltà a pensare le strutture specifiche, e a liberarsi della metodologia di pensare tipica dei linguaggi che non consentono tale livello di astrazione.

Abbiamo visto nell'introduzione, come un linguaggio di buon successo, deve essere tale che se i programmi sorgenti, esaminati da uno strumento software, vengono evidenziati gli errori sintattici.
La tendenza attuale di astrarre, impedisce questi controlli automatici. Infatti se lo stesso operatore funziona ugualmente su più tipi (overloading), ad esempio la somma tra interi, tra interi e reali, tra liste etc…, non è possibile individuare un eventuale errore, soltanto controllando i tipo degli operandi.
Se in Pascal si definisce un tipo che è un subrange di un altro tipo, ad esempio 1..44, questo che proprietà deve avere? E' corretto che abbia un meccanismo di ereditarietà (soluzione del Pascal) tale che si porta appresso tutte le caratteristiche (vedi operatori) del tipo da cui deriva? Anche questa è una causa di impossibilità di verifica.
Inotre le possibilità del polimorfismo (una variabile nasce carattere e poi diventa un intero a secondo dell'introduzione da tastiera) offre enormi potenzialità, ma non sono più controllabili automaticamente.
Molti algoritmi sono intrinsecamente generici; quindi se un linguaggio permette di sfruttare algoritmi generici, offre l'enorme potenzialità di poter ri-sfruttare lo stesso codice per dati di tipo diverso. Ad esempio un programma di sorting, stabilito il criterio con cui mettere in ordine, lavora allo stesso modo sia che debba ordinare stringhe che numeri interi.

Overloading (sovrapposizione)

In qualunque normalissimo linguaggio (ad esempio il Pascal) un operatore ha più di un significato. Ad esempio l'operatore meno ('-') ha cinque significati diversi: negazione intera (-5), negazione reale (-0.5), sottrazione intera (5-3), sottrazione reale (5.0-3.0), differenza di insiemi.
Il compilatore deve di volta in volta capire il significato dell'operatore, nel contesto.

In Ada sia gli identificatori che gli operatori possono essere dotati dal programmatore, di questa proprietà. Ad esempio l'operatore '/' indica una divisione intera e una divisione reale. Quindi 7/2 da 3, mentre 7.0/2.0 da 3.5. Ma si può anche aggiungere un significato ulteriore:


function "/" (m,n: integer) return Float is
begin
	return float (m)/float (n);
end;

quindi 7/2 da 3.5.
In generale, si I è un'identificatore o operatore che si riferisce sia ad una funzione f1 di tipo S1 -> T1 sia ad una funzione f2 di tipo S2 -> T2, si hanno due tipi di overloading:

Le due scelte sono in conflitto tra loro: o l'una o l'altra.

Un esempio di sovraccarico è il write del Pascal: all'interno delle parentesi si può mettere tutto (caratteri, interi, reali etc...). Questa caratteristica risulta indispensabile, qualora si vuole ampliare l'operazione a tipi che ancora devono essere inventati.

Polimorfismo e tipi parametrizzati

In un sistema con tipi polimorfici si possono mettere a punto astrazioni che operano uniformemente su una intera famiglia di tipi. Nel Pascal, la struttura Set è polimorfica, perché la si può applicare o qualunque tipo (insiemi di interi, insiemi di caratteri etc...).
Fra i linguaggi più recenti occupa una posizione di rilievo ML il quale possiede una forma particolare chiamata polimorfismo parametrico. In ML viene detto polimorfismo ad hoc l'overloading, e polimorfismo inclusivo l'ereditarietà.
Supponiamo di avere una funzione che applicata ad x e a y da il valore corrispondente al secondo:


fun second (x: int y: int) = y

E' anche possibile scrivere


fun second (x: a, y: b) = y

Dove a e b sono due qualunque tipi. Questa è una funzione che è capace di gestire qualunque tipo e può restituire un qualunque tipo.

Questo articolo è stato scaricato dal Club di informatica
Pagina curata da:
Luca Sabatucci