Per determinare se un termine Γ¨ ben tipato o meno si procede cercando di costruire un albero di derivazione ma, nel farlo, non ci Γ¨ sempre chiaro come procedere in maniera sistematica, come abbiamo visto nell’esempio di prima. Ecco che quindi vogliamo costruire un algoritmo che ci permetta di farlo sistematicamente: l’algoritmo di inferenza.

L’idea generale dell’algoritmo di inferenza Γ¨ la seguente: innanzitutto ci dotiamo di un sistema grafico che ci permetta di operare facilmente sulle -espressioni, quindi proviamo a β€œintuire” quali tipi potremmo assegnare ai sotto-termini della -espressione, creando dei collegamenti che ci permettano di porli in relazione tra di loro, e infine proviamo a trovare una soluzione.

Fase 1 dell’algoritmo

Come giΓ  detto, per il primo passo di questo algoritmo di inferenza ci serve un modo β€œvisivamente comodo” per rappresentare le -espressioni. Per farlo, scegliamo una struttura ad albero esattamente come abbiamo fatto per l’albero di derivazione, infatti questa nuova struttura sarΓ  facilmente β€œtraducibile” poi in un albero di derivazione proprio perchΓ© sono entrambi della forma ad albero.

Questa struttura verrΓ  chiamata albero sintattico.

Definizione: albero sintattico

Data una -espressione , l’albero sintattico Γ¨ una struttura ad albero definita induttivamente sulla forma sintattica del termine :

  1. Se Γ¨ un termine della forma di una variabile , allora sarΓ : 500
  2. Se Γ¨ un termine della forma di una costante , allora sarΓ : 500
  3. Se Γ¨ un termine della forma di un’astrazione , allora sarΓ : 500
  4. Se Γ¨ un termine della forma di un’applicazione , allora sarΓ : 500
  5. Se Γ¨ un termine della forma di una struttura di controllo , allora sarΓ : 500

Esempio di albero sintattico

Ecco un esempio di albero sintattico della -espressione :

500

Esercizio 1 di costruzione di un albero sintattico

Costruisci l’albero sintattico della -espressione .

Esercizio 2 di costruzione di un albero sintattico

Costruisci l’albero sintattico della -espressione .

Fase 2 dell’algoritmo

Dopo aver costruito l’albero sintattico, si fa una visita dell’albero usando la strategia bottom-up (quindi partendo dalle foglie e arrivando alla radice): ogni nodo viene β€œannotato” con un’espressione di tipo, ossia delle β€œetichette” che ci aiutano a intuire quale tipo potrebbe assumere quel nodo.

Definizione: espressione di tipo

Sia l’insieme infinito di variabili di tipo.

Un’espressione di tipo Γ¨ un’etichetta assegnata a un nodo dell’albero sintattico che corrisponde a una stringa ben formata a partire dalla seguente grammatica espressa in BNF:

dove:

Per determinare delle β€œrelazioni” tra le varie espressioni di tipo usiamo dei vincoli che devono essere rispettati affinchΓ© la -espressione sia ben tipata.

Definizione: vincolo

Un vincolo Γ¨ una relazione di uguaglianza tra due espressioni di tipo e :

Proviamo quindi a capire come annotare le espressioni di tipo sull’albero sintattico distinguendo tra i vari casi.

Nel caso di un termine della forma di una variabile , nel nodo ci annotiamo un segnaposto , avendo cura di usare lo stesso segnaposto per tutte le occorrenze di e solo per quelle:

500

Nel caso di un termine della forma di una costante , nel nodo ci annotiamo il tipo booleano :

500

Nel caso di un termine della forma di un’astrazione , partendo dal basso (perchΓ© stiamo usando la strategia bottom-up):

Il risultato Γ¨ il seguente:

500

Nel caso di un termine della forma di un’applicazione , partendo sempre dal basso (perchΓ© stiamo usando la strategia bottom-up):

  • Dal sotto-albero avremo ottenuto un’espressione di tipo (dove Γ¨ diverso dal usato nell’astrazione , perchΓ© questi due sotto-termini non sono la stessa cosa).
  • Dal sotto-albero avremo ottenuto un’espressione di tipo .
  • Nel nodo ci annotiamo un nuovo segnaposto .
  • Generiamo un nuovo vincolo .

Il risultato Γ¨ il seguente:

espressione-di-tipo-applicazione

In particolare, se Γ¨ di un certo tipo funzione , allora il nodo ha tipo e il vincolo generato Γ¨ :

espressione-di-tipo-applicazione2

Nel caso di un termine della forma di una struttura di controllo :

  • Dal sotto-albero avremo ottenuto un’espressione di tipo .
  • Dal sotto-albero avremo ottenuto un’espressione di tipo .
  • Dal sotto-albero avremo ottenuto un’espressione di tipo .
  • Generiamo il vincolo che richiede che il tipo di sia booleano: .
  • Generiamo il vincolo che richiede che il tipo di e di coincida: .
  • Nel nodo ci annotiamo il tipo (), che sarΓ  proprio il tipo che restituisce la struttura di controllo.

Il risultato Γ¨ il seguente:

espressione-di-tipo-struttura-di-controllo

Fase 3 dell’algoritmo

Nella terza fase dell’algoritmo di inferenza, una volta che l’albero sintattico Γ¨ stato annotato, si cerca di capire se questo insieme di vincoli ammette una soluzione e si cerca di trovare quella generale da cui derivare tutte le altre.

All’inizio della terza fase ci ritroviamo con un sistema di vincoli:

Per risolvere questo sistema usiamo un algoritmo di risoluzione.

Algoritmo di risoluzione

Dato un sistema di vincoli , l’algoritmo di risoluzione permette di risolvere , ottenendo un successo o un fallimento.

I passi da seguire sono i seguenti:

  1. Per ogni vincolo , verificare se nella seguente tabella il vincolo rispetta una delle seguenti forme e condizioni e, eventualmente, eseguire la trasformazione indicata:
Forma del vincoloCondizioneCosa fare
Γ¨ un’espressione di tipo qualsiasiEliminare il vincolo
Γ¨ un’espressione di tipo qualsiasi e Γ¨ un segnapostoRimpiazzare il vincolo con
e sono due espressioni di tipo qualsiasiRimpiazzare il vincolo con e
e sono due espressioni di tipo qualsiasi, Γ¨ il tipo booleanoL’algoritmo fallisce per errore di tipo
e sono due espressioni di tipo qualsiasi, Γ¨ il tipo booleanoL’algoritmo fallisce per errore di tipo
Γ¨ un’espressione di tipo qualsiasi, Γ¨ un segnaposto e compare in L’algoritmo fallisce per errore di occorrenza
Γ¨ un’espressione di tipo qualsiasi, Γ¨ un segnaposto e non compare in Rimpiazzare le occorrenze di con in tutti gli altri vincoli e lasciando questo invariato
  1. Ripetere il passo 1 finchΓ© non si ottiene un fallimento dell’algoritmo o finchΓ© non Γ¨ piΓΉ possibile applicare le trasformazioni indicate.
  2. Se non Γ¨ avvenuto un fallimento dell’algoritmo, allora ha avuto successo e si Γ¨ risolto il sistema .

Osservazione: invarianza dell'ordine delle trasformazioni

L’ordine in cui si applicano le trasformazioni del passo 1 dell’algoritmo di risoluzione non influisce sul risultato.

Con il successo dell’algoritmo di risoluzione, avremo ottenuto un nuovo sistema di vincoli

dove ogni Γ¨ un segnaposto (che compare una sola volta nel sistema) corrispondente esattamente a un tipo che sarΓ  uguale a un’espressione di tipo .

A questo punto non ci rimane altro che fare una sostituzione di ogni con il rispettivo .

Definizione: sostituzione

Nell’algoritmo di inferenza, una sostituzione Γ¨ una funzione da variabili di tipo a espressioni di tipo in cui, dato un tipo , sostituiamo in ogni occorrenza di ogni sotto-tipo con la sostituzione .

Da questa sostituzione otteniamo una possibile soluzione dell’algoritmo di inferenza.

Definizione: soluzione

Nell’algoritmo di inferenza, dato un sistema di vincoli e una sostituzione , diciamo che Γ¨ una soluzione (o unificatore) del sistema se, per ogni :

Attenzione: differenza tra gli nella definizione di soluzione

Nella definizione di soluzione, i due che compaiono rispettivamente nel sistema di vincoli e nella relazione di uguaglianza non significano la stessa cosa: mentre nel primo caso serve solo come β€œpromemoria” per ricordarsi di come sono β€œcollegate” tra di loro le espressioni di tipo, nel secondo indica una vera e propria uguaglianza tra i risultati delle sostituzioni.

In particolare, usando l’algoritmo di risoluzione per β€œridurre al minimo” il sistema di vincoli, otterremo non una soluzione qualsiasi ma la soluzione piΓΉ generale, che ci permette di ricavare a partire da essa tutte le altre possibili soluzioni.

Definizione: soluzione piΓΉ generale

Nell’algoritmo di inferenza, dato un sistema di vincoli e una soluzione , diciamo che Γ¨ la soluzione piΓΉ generale (o unificatore piΓΉ generale) del sistema se ogni soluzione del sistema Γ¨ ottenibile componendo con un’altra sostituzione.

Definizione dell’algoritmo

Dopo aver esplicitato il funzionamento dell’algoritmo di inferenza attraverso le 3 fasi di cui si compone, possiamo finalmente dichiararlo formalmente.

Algoritmo di inferenza

Data una -espressione , l’algoritmo di inferenza permette di inferire il tipo di .

I passi da seguire sono i seguenti:

  1. Costruire l’albero sintattico .
  2. Su ogni nodo dell’albero sintattico annotare delle espressioni di tipo e generare i rispettivi vincoli seguendo questa tabella:
Contenuto del nodoCosa annotareVincoli da generareRisultato sull’albero sintattico
Termine della forma di una variabile Nel nodo : un segnaposto , avendo cura di usare lo stesso segnaposto per tutte le occorrenze di e solo per quelleespressione-di-tipo-variabile
Termine della forma di una costante Nel nodo : il tipo booleano espressione-di-tipo-costante-booleana
Termine della forma di un’astrazione Nel nodo : un tipo funzione , dove Γ¨ un segnaposto (e, per ogni occorrenza di in , dovremo usare sempre lo stesso segnaposto ) e Γ¨ l’espressione di tipo del sotto-albero espressione-di-tipo-astrazione
Termine della forma di un’applicazione con con espressione di tipo qualsiasiNel nodo : un segnaposto , dove Γ¨ l’espressione di tipo del sotto-albero e Γ¨ l’espressione di tipo del sotto-albero espressione-di-tipo-applicazione
Termine della forma di un’applicazione con con espressione di tipo Nel nodo : il tipo , dove Γ¨ l’espressione di tipo del sotto-albero espressione-di-tipo-applicazione2
Termine della forma di una struttura di controllo Nel nodo : il tipo , dove Γ¨ l’espressione di tipo del sotto-albero , dove Γ¨ l’espressione di tipo del sotto-albero

, dove Γ¨ l’espressione di tipo del sotto-albero e Γ¨ l’espressione di tipo del sotto-albero
espressione-di-tipo-struttura-di-controllo
  1. Sul sistema di vincoli usare l’algoritmo di risoluzione.
  2. Se l’algoritmo di risoluzione ha fallito, allora la -espressione non Γ¨ ben tipata.
  3. Se l’algoritmo di risoluzione ha avuto successo, effettuare su ogni vincolo del nuovo sistema la sostituzione.
  4. Il risultato del passo 5 Γ¨ la soluzione piΓΉ generale del sistema.

Estensioni dell’algoritmo

L’algoritmo di inferenza puΓ² essere esteso a versioni del -calcolo con ulteriori tipi.

Estensione con i numeri interi

Possiamo, per esempio, aggiungere tra i tipi anche i numeri interi , sotto forma del tipo :

Di conseguenza, nella definizione del termine basterΓ  specificare che le costanti potranno assumere come valori anche i numeri interi :

e, nella forma sintattica della struttura di controllo , potrΓ  assumere solo i valori e .

Nell’algoritmo di inferenza non si notano particolari differenze, ma:

  • Tra le espressioni di tipo, annoverare tra le possibili forme anche .
  • Nella tabella di generazione dei vincoli, specificare che ai termine della forma di una costante va assegnato il tipo giusto a seconda del valore che assume, quindi:
  • Nell’algoritmo di risoluzione, aggiungere che, se c’è un vincolo della forma o o o allora l’algoritmo fallisce perΒ errore di tipo.

Estensione con le liste

Questa volta, tra i tipi annoveriamo anche le liste, costituite da elementi di tipo :

Ovviamente, ciΓ² implica che anche tra le espressioni di tipo dobbiamo annoverare tra le possibili forme anche .

Nei valori possibili che puΓ² assumere un termine-costante, ci includiamo i due costruttori canonici delle liste, la lista vuota e il cons :

In particolare, in Haskell il costruttore lista vuota ha tipo e il costruttore cons ha tipo :

Tenendo conto che nella stessa -espressione possiamo costruire liste di elementi di tipo diverso (per esempio, possono coesistere in una -espressione una lista di tipo e una lista di tipo ), abbiamo che le costanti dei costruttori canonici delle liste (cioè e ) devono essere interpretate come costanti polimorfe, cioè in ogni loro occorrenza possono assumere tipo diverso.

Per questo motivo, nella fase di generazione dei vincoli dell’algoritmo di inferenza, per ogni occorrenza di uno di questi due costruttori dobbiamo stare attenti ad annotare nel nodo tipi che usano segnaposti freschi: se, per esempio, in un nodo trovo , lo annoto come una lista di elementi che hanno come tipo un segnaposto ; se lo troverΓ² una seconda volta, lo annoterΓ² come una lista di elementi che hanno come tipo un segnaposto e cosΓ¬ via:

Per quanto riguarda la fase di risoluzione dei vincoli, se abbiamo vincoli che impongono l’uguaglianza di tipi che hanno forme palesemente diverse (per esempio , o ) allora l’algoritmo fallisce per errore di tipo.

Abbiamo anche un caso analogo a quello dei tipi funzioni in cui possiamo semplificare l’uguaglianza di due funzioni () imponendo l’uguaglianza dei domini e dei codomini ( e ): se c’è un vincolo allora possiamo rimpiazzarlo con .

Estensione con funzioni di libreria

In generale, possiamo estendere l’algoritmo di inferenza assumendo che nelle espressioni da tipare compaiano le funzioni della libreria standard di Haskell (es. id, head, tail, …), che potranno essere funzioni polimorfe (ossia il loro tipo non Γ¨ un tipo specifico ma Γ¨ una variabile di tipo) e per trattarle in questo modo va fatto come abbiamo appena fatto per le liste: ogni occorrenza deve usare nuove variabili di tipo (segnaposti) per ogni occorrenza.

Estensione con definizioni ricorsive

Potremmo volere stabilire il tipo di una definizione ricorsiva della forma

dove puΓ² comparire in .

Quel che dobbiamo fare Γ¨ aggiungere il vincolo dove Γ¨ la variabile di tipo associata a e Γ¨ l’annotazione di .


Fonti