Premessa

Ciao! Se è la prima volta che capiti su questo sito, ti consiglio di consultare la pagina principale di questo cosiddetto Giardino Digitale per scoprire meglio cos’è e come navigarlo.

Lo stato di questa nota è al momento: 🔴 Bozza.


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 (fase 1 dell’algoritmo), 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 (fase 2 dell’algoritmo), e infine proviamo a trovare una soluzione (fase 3 dell’algoritmo).

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

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 .

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

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

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

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

5 - Estensioni dell’algoritmo

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

5.1 - 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 oppure oppure oppure allora l’algoritmo fallisce per errore di tipo.

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

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

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