• Non ci sono risultati.

Capitolo 6 Esempi e Tests 6.1 InferW all’opera: un esempio concreto

N/A
N/A
Protected

Academic year: 2021

Condividi "Capitolo 6 Esempi e Tests 6.1 InferW all’opera: un esempio concreto"

Copied!
20
0
0

Testo completo

(1)

Capitolo 6

Esempi e Tests

6.1 InferW all’opera: un esempio concreto

Per capire davvero come funziona inferW (la versione di W implementata per Sigma_X), vediamo come opera su un termine del linguaggio usando come riferimento la versione in pseudo-codice (OCaml-Haskell like) riportata in Appendice C. Si noti che la versione realizzata in Java segue fedelmente questa ultima, tranne per il fatto che risulta meno leggibile (i linguaggi funzionali sono molto più espressivi).

Il termine che prendiamo in considerazione è il seguente:

[uno=@(x)x , due=@(x)[tre=@(x,y)x]]

In inferW :: (Env x Term→ Substitution x Type) trovo il match Object(fi , A , ls). C’è subito un’annotazione da fare riguardante il parametro A (nome della classe): ogni classe ha un tipo associato, il tipo classe (oggetto nel nostro calcolo). Il tipo classe è un tipo ricorsivo (µ id.e) ed è realizzato (implementato) utilizzando un nome (“puntatore”) che opera come un self-reference. Il nome appena creato (per le regole date) deve essere legato immediatamente al self di ogni metodo (nel codice è prevista una procedura per la propagazione del nome).

Operiamo un calcolo step by step: | Object(fi, A, ls) →

(2)

Cos’ è il parametro ls nel nostro caso? SelMetPairs = [(Sel_name, Term)]

Vediamo nello specifico ls

ls = { (uno, @(x)x) , (due , @(x)[tre=@(x,y)x]) }

Troviamo subito la definizione della funzione parTypes:

let parTypes = \(l m) (match m with

Method (fi,fl,by) (l , (normalize fi fl))

Analizziamo uno per uno gli elementi

Ÿ fl : lista dei parametri formali. Formals = [(Var_name , TypeA)]. Ÿ by : body, corpo del metodo.

Ÿ l : selettore. Ÿ m : metodo.

Ci chiediamo adesso: come funziona la normalize? E’ una funzione definita nel seguente modo: [(Var_name , TypeA)] TypeB.

Notiamo che i TypeB formano il sistema dei tipi esteso con Top (fallimento) e variabili di tipo.

La parTypes viene chiamata in

let (ly , list) = unzip (map parTypes ls) in

(3)

ls = { (uno, @(x : Obj_1)x) , (due , @(x : Obj_1)[tre=@(x,y)x]) }

La parTypes chiamata sulla ls produce:

Ÿ Per uno : [(uno , Obj_1→var0)]

Ÿ Per due : [(due , Obj_1var1)]

Dopo la chiamata di let(ly,list)….ottengo:

Ÿ ly = [uno , due]

Ÿ list = [Obj_1var0 , Obj_1var1]

Calcoliamo adesso la sostituzione S

let S = fold (\r ts. r.mgu(Obj_1 , (Class ts)) [ ] (list)

Dunque leghiamo a r e ts parametri dell’ astrazione funzionale rispettivamente: r : [ ], ts : (list). La S ottenuta è la sostituzione identità perché il self per ogni elemento è esattamente Obj_1.

Controlliamo adesso l’unicità del self con:

If ( S == top) then error fi “type mismatch in class self” else

Creiamo il tipo oggetto in due passi:

let objT = zip ly S(list) in

(4)

Nel nostro esempio essendo S la sostituzione identità ottengo:

ObjT = (uno , Obj_1var0) , (due , Obj_1var1)

Trovo: let lstype = TyObj(objT) in

Creo allora: lstype = [(uno , Obj_1→var0) , (due , Obj_1var1)].

Cominciamo ad aggiungere i binding al contesto:

let ctx’ = addBinding ctx Obj_1 lstype in

Come si è ben capito questo è un passo chiave: al contesto viene aggiunto un nuovo binding che lega Obj_1 (nome di classe/oggetto) al tipo (oggetto) appena calcolato:

Obj_1 = [(uno , Obj_1var0) , (due , Obj_1var1)]

Andiamo avanti:

let ctx’’ = fold(\ctx (l, ltype). addBinding ctx l ltype) ctx’ objT in

Aggiungiamo al contesto dova già avevamo il legame per la classe Obj_1 (ctx‘), un legame per ogni coppia (selettore , tipo).

Vediamo brevemente un dump di ctx’’:

Obj_1 [uno:Obj_1var0,due:Obj_1var1]

Uno Obj_1var0

(5)

Notiamo che var0 e var1 sono ancora variabili di tipo da istanziare. Inoltre osserviamo che l’unico contesto realmente esistente (globale) al momento della chiamata è ctx (quello passato a inferW al momento della chiamata). ctx’ e ctx’’ sono contesti (ambienti) locali.

Giunti a questo punto calcoliamo il tipo del corpo tramite la chiamata della funzione

byType

let Sf = fold byType S ls in

Vediamo come si comporta tale funzione:

let byType = \sub D(l , Method(fi,fl,by))

Come parametri le vengono passati una sostituzione e una coppia (Selettore , Metodo).

Nel passaggio successivo viene reperito dal contesto, per mezzo del selettore, il tipo del metodo associato:

let Ts = getTypeFromCtx fi ctx’’ l in

Ÿ Per uno : Ts = Obj_1→var0

Ÿ Per due : Ts = Obj_1var1 let (Npl , Tpl) = unzip fl in

In questo passaggio viene “unzippata” la lista dei parametri formali in (Nome , Tipo):

(6)

Ÿ Per due : (Npl , Tpl) = (x , Obj_1)

let fl* = zip Npl (Dom Ts) in

In questo passaggio il nome dei parametri formali viene compattato con il dominio del tipo(i) Ts appena calcolato:

Ÿ Per uno : fl* = (x , Obj_1) Ÿ Per due : fl* = (x , Obj_1)

Da questo punto in poi occupiamoci in maniera distinta di uno e due. Trattiamo tanto per cominciare uno.

let ctx* = fold (\ctx P(x , type). addBinding ctx x type) sub(ctx’’) fl* in

sub(ctx’’) = ctx’’ perché S è la sostituzione identità calcolata in precedenza.

Vediamo cosa contiene ctx* :

Obj_1 [uno:Obj_1var0,due:Obj_1var1]

Uno Obj_1var0

Due Obj_1var1

X Obj_1

let (S , T) = inferW ctx* by in

(7)

La chiamata (ricorsiva) di inferW va a trovare: | Var(fi , i)→ ([ ] , getTypeFromCtx fi ctx i).

Dunque, ad x viene assegnato tipo Obj_1 (come si nota dal contesto (tabella)): S = [ ] , T = Obj_1.

Ultimo passo. Componiamo: sub.S.mgu((Range Ts) , T), dove:

Ÿ sub = [ ] Ÿ s = [ ]

Ÿ Range Ts = var0

A questo punto è facile calcolare il most general unifier tra var0 e Obj_1 : otteniamo la sostituzione [var0 / Obj_1] che diventa il primo elemento di Sf.

Il calcolo del corpo di due è leggermente più complicato perché contiene un oggetto. Il contesto di valutazione (ctx*) è lo stesso di quello per uno eccetto per il fatto che var0 è stata istanziata con la sostituzione appena calcolata (cioè sostituita da Obj_1).

Procediamo al solito per passi:

let (S , T) = inferW ctx* by in

by ha la forma : [tre=@(x,y)x]

Dunque inferW “matcha” | Object(fi, A, ls)

Come prima cosa leghiamo il self del nuovo oggetto al suo nome (nuovo anch’esso):

(8)

Ricordiamo che il contesto di riferimento è ctx*. Percorriamo velocemente i passi:

let (ly , list)…..

Ÿ ly = [tre]

Ÿ list = [Obj_2var2var3]

Otteniamo S sostituzione identità e passiamo anche il controllo per il self.

let objT…

Ÿ objT = (tre , Obj_2var2→var3) lstype = [ tre : Obj_2var2→var3]

Il ctx’’ che otteniamo è il seguente:

Obj_1 [uno:Obj_1Obj_1,due:Obj_1var1]

Uno Obj_1Obj_1

Due Obj_1var1

X Obj_1

Obj_2 [tre:Obj_2var2var3]

Tre Obj_2var2var3

Adesso costruiamo il tipo del suo corpo

(9)

Ÿ S = [ ]

Ÿ ls = [(tre , @(x : Obj_2,y)x)]

let byType.. let Ts….

Ÿ Per tre : Obj_2var2var3 let (Npl , Tpl)…

Otteniamo le due coppie: (x , Obj_2) , (y , var2).

let fl*….

Ÿ fl* = [(x , Obj_2) , (y , var2)]

let ctx*…..

sub (ctx’’) = ctx’’ perché sub vale [ ]

Vediamo che forma ha ctx*

Obj_1 [uno:Obj_1Obj_1,due:Obj_1var1]

Uno Obj_1Obj_1

Due Obj_1var1

X Obj_1

Obj_2 [tre:Obj_2var2var3]

Tre Obj_2var2var3

X Obj_2

(10)

let (S , T) = inferW ctx* by in

Ÿ by vale x

Dunque otteniamo: S = [ ] , T = Obj_2.

Calcoliamo la sostituzione finale con : sub.S.mgu(var3 , Obj_2).

Ÿ sub = [ ] Ÿ S = [ ]

Ÿ mgu(var3 , Obj_2) = [var3 / Obj_2].

Per tre ho allora prodotto la sostituzione Sf = [var3 / Obj_2].

InferW mi restituisce : (Sf , [tre:Obj_2var2Obj_2]).

Ritorno adesso al momento della chiamata per due:

let (S , T) = ( [var3 / Obj_2] , [tre:Obj_2var2Obj_2] ).

Notiamo che il tipo della classe legato a T è Obj_2.

Siamo davvero alla fine…Componiamo le sostituzioni con:

sub.S.mgu((Range Ts) , Obj_2)

Otteniamo: Ÿ sub = [ ] Ÿ s = [ ]

(11)

Come risultato finale dell’inferenza ho dunque:

Ÿ Sf = ( [ var0 / Obj_1] , [var3 / Obj_2] , [var1 / Obj_2] )

Ÿ Sf(lstype) = Obj_1 = [uno:Obj_1Obj_1 , due:Obj_1Obj_2].

6.2 Esempi di applicazione: breve raccolta

Notiamo che il primo parametro di ogni metodo è legato automaticamente dall’algoritmo di inferenza (inferW) al nome della classe generato in fase di costruzione dell’albero della sintassi astratta. Dunque, il suddetto (che andrà legato al self) va sempre esplicitato, mentre non ne va indicato il tipo (se ne occuperà l’algoritmo). Notiamo che qualunque tipo venga assegnato dall’utente al self, questo verrà forzato a contenere il nome della classe.

Ÿ [uno=@(x)x]

Un oggetto molto molto semplice, il cui corpo è costituito da un solo metodo che non fa altro che calcolare l’identità.

Il tipo inferito dall’algoritmo è della forma [uno:Obj_0→Obj_0]. La traccia contiene la sola sostituzione (var0 , Obj_0), visto che l’unica variabile generata è quella per il rango (var0), istanziata poi con Obj_0 (nome della classe).

Ÿ [uno=@(x)x].uno()

Stesso oggetto di prima con la differenza che questa volta è stata applicata la regola per la Select. Ciò vuol dire che all’oggetto (Obj_0) è stato applicato il metodo uno(). Il tipo inferito dall’algoritmo è della forma Obj_0. La traccia contiene le sostituzioni (var0 , Obj_0) e (var1 , Obj_0) ed il tipo parziale (che sarebbe il tipo dell’oggetto) [uno:Obj_0→Obj_0].

(12)

Ÿ [uno=@(x , y)y]

Anche questo è un oggetto semplice che ha come corpo un unico metodo che prende due parametri: il primo rappresenta il self, del secondo non viene esplicitato il tipo: sarà compito della Normalize assegnargli una variabile di tipo (variabile fresh). Il tipo inferito dall’algoritmo è della forma [uno:Obj_0var1→var1]. La traccia contiene l’unica sostituzione (var0 , var1). In questo caso il tipo del range non è specificato. Il tipo parziale calcolato è: [uno:Obj_0var1→var0].

Ÿ [uno=@(x , y)y].uno()

Sull’oggetto tipato in precedenza viene chiamato il metodo uno: questo viene fatto però in modo errato visto che il metodo nella sua segnature richiede almeno un parametro. Proprio per questo motivo il risultato dell’applicazione dell’inferenza è il laconico “Error type class”. La traccia mi dice: “Impossibile inferire un tipo: Top

Substitution”.

Ÿ [uno=@(x , y)x , due=@(x , y:INT)y]

Questo esempio mostra invece un oggetto il cui corpo è costituito da due metodi identificati dai selettori uno e due. Tali metodi oltre al self, prevedono nella loro segnature un altro parametro: in uno il tipo di questo ultimo non è specificato, mentre in due gli viene assegnato tipo INT. Uno restituisce come risultato se stesso (identità), due restituisce y. Il tipo inferito dall’algoritmo è della forma

[uno:Obj_0var1→Obj_0 , due:Obj_0INT→INT]. Da notare in questo caso è come al self di due venga legato Obj_0 visto che appartiene alla stessa classe di uno. La traccia contiene le sostituzioni (var0 , Obj_0) e (var2 , INT) risultanti delle unificazioni dei range. Il tipo parziale calcolato è: [uno:Obj_0var1→var0 ,

(13)

Ÿ [uno=@(x , y)x , due=@(x , y:INT)y].tre()

Sull’oggetto visto in precedenza viene invocato il metodo tre che però non è stato definito. L’inferenza mi dirà “Errore: selettore assente!”. La traccia mi dirà che l’inferenza non è stata applicata correttamente.

Ÿ [uno=@(x , y)x].uno(y)

Ritorniamo ad un oggetto simile ai primi esempi visti. Questa volta il metodo uno viene invocato correttamente, cioè con il parametro.

Il tipo inferito dall’algoritmo è della forma Obj_0 (il range del tipo dell’oggetto). La traccia contiene le sostituzioni (var0 , Obj_0), (var2 , VOID) e (var1 , Obj_0) oltre al tipo parziale [uno:Obj_0var2→Obj_0]. Notare che il tipo VOID contenuto nella seconda sostituzione non è altro che il tipo associato al parametro attuale y che è ovviamente distinto dal parametro formale y (dimostra che gli ambienti funzionano correttamente).

Ÿ [uno=@(x)m]

Situazione d’errore. L’algoritmo non riesce ad identificare il nome (identificatore) m e restituisce “Error type class”.

Ÿ [uno=@(x , y)y.x()]

Anche questa è una situazione d’errore. L’oggetto in questione ha un solo metodo identificato da uno, che prende un parametro oltre al self e che nel suo corpo tenta di invocare il self, erroneamente interpretato come metodo, su questo. Naturalmente la risposta dell’algoritmo di inferenza sarà “Error type class”.

(14)

• [uno=@(x , y:REAL )y , due=@(x , y:INT)[tre=@(x , y:STRING)y]]

Presentiamo adesso qualcosa di più complicato. Il corpo di questo oggetto è costituito da due metodi identificati dai selettori uno e due: il primo metodo oltre al self prevede un secondo parametro di tipo REAL, il secondo ha a sua volta un corpo che è un oggetto costituito da un metodo con selettore tre. Si notano da questo esempio alcuni aspetti interessanti.

Vediamo come prima cosa che il tipo calcolato dall’inferenza è:

[uno:Obj_1REAL REAL , due:Obj_1INT [tre:Obj_0STRING STRING]].

L’oggetto che rappresenta il corpo di due è un nuovo oggetto che di conseguenza avrà un nome nuovo: infatti il nome calcolato per lui dall’algoritmo di inferenza è Obj_0 mentre quello calcolato per l’oggetto “Top” (principale) è Obj_1. La traccia mi restituisce le sostituzioni (var0 , REAL) , (var1 , [tre:Obj_0STRING STRING]) e (var2 , STRING). Il tipo parziale calcolato è invece: [uno:Obj_1REAL var0 , due:Obj_1INT var1].

• [uno=@(x , y)[due=@(x , y:INT)x]]

E’ un semplice oggetto sulla falsa riga dell’esempio precedente. Contiene un solo metodo rappresentato dal selettore uno. Tale metodo ha come un altro oggetto contenente a sua volta un solo metodo identificato dal selettore due. Il tipo inferito dallo algoritmo ha la forma: [uno:Obj_1var1 [due:Obj_0INT INT]]. La traccia contiene le sostituzioni (var0 , [due:Obj_0INT Obj_0]) , (var2 , Obj_0). Il tipo parziale calcolato è: [uno:Obj_1var1 var0].

Ÿ [uno=@(x)x , due=@(x)[tre=@(x , y)x]].due()

(15)

calcolare la classica identità, mentre il metodo due ha come corpo un nuovo oggetto con un solo metodo contrassegnato dal selettore tre.

Il tipo inferito dall’algoritmo è della forma [tre:Obj_0var4→Obj_0]. Questo avviene perché: il tipo calcolato per tre costituisce il range del tipo calcolato per due. La traccia contiene le sostituzioni (var0 , [tre:Obj_0var4Obj_0]), (var1 , Obj_1), (var2 , [tre:Obj_0var4 Obj_0]) e ( var3 , Obj_0). Il tipo parziale calcolato ha la forma: [uno:Obj_1 Obj_1 , due: Obj_1 [tre:Obj_0var4 Obj_0]].

Ÿ [uno=@(x)x , due=@(x)[tre=@(x , y)x]].due().tre(y)

Altro esempio molto interessante perché mostra l’inferenza per una Select a due livelli di profondità. Si accede al selettore tre passando prima per due visto che l’oggetto che contiene tre è il corpo di due. Al metodo tre viene passato un parametro, mentre due come unico parametro ha l’esplicito self.

Il tipo inferito dall’algoritmo è della forma Obj_0 visto che Obj_0 è proprio il rango di tre. La traccia contiene le sostituzioni (var1 , [tre:Obj_0var5 Obj_0]), (var2 , Obj_1), (var3 , [tre:Obj_0var5 Obj_0]) e (var4 , Obj_0). Il tipo parziale calcolato è [tre:Obj_0var5→Obj_0].

Ÿ [uno=@(x)x , due=@(x , y)x].due(@(x)x)

Questo esempio mostra una della caratteristiche in più di Sigma_X rispetto a Sigma: i metodi possono essere passati come parametri ad altri metodi. Sull’oggetto in questione che possiede due metodi viene invocato il metodo due che ha come parametro il metodo che calcola l’identità.

Il tipo inferito dall’algoritmo è della forma Obj_0 visto che il range di due ha appunto questo tipo. La traccia contiene le sostituzioni (var1 , Obj_0), (var4 , var6 var6), (var2 , Obj_0), (var3 , Obj_0) e (var5 , var6). Questa ultima sostituzioni è dovuta all’inferenza applicata a TreeAbst (metodo): il tipo calcolato per questo ultimo è

(16)

infatti var5→var6. Altra sostituzione interessante è la seconda che contiene come tipo lo schema calcolato per il metodo passato come parametro attuale.

• [uno=@(x , y)x].uno(@(x:INT, y, c:STRING)c)

Altro esempio che mostra il passaggio di un metodo come parametro. Questa volta la situazione è leggermente più complessa. Infatti il metodo parametro di uno presenta ben tre parametri formali e restituisce come risultato una stringa. Il tipo inferito dallo algoritmo è Obj_1 mentre la traccia contiene le sostituzioni (var0 , Obj_1), (var2 , INTvar4STRING STRING), (var1 , Obj_1), (var3 , STRING). La sostituzione più interessante è ovviamente quella che contiene lo schema di tipo per il metodo parametro (la seconda). Il tipo parziale calcolato è: [uno:Obj_1var2 Obj_1]. Notare che lo scoping operato dal sistema è corretto visto che ad y viene assegnata prima var2 e poi var4 (sono due y distinte).

• [uno=@(x , y)y].uno(@(x:INT, y, c:STRING)c)

Consideriamo lo stesso oggetto dell’esempio precedente con una piccola modifica: sostituiamo la x presente nel corpo del metodo uno con la y, suo secondo parametro formale. Adesso il tipo inferito ha la forma: INTvar4STRING STRING. La traccia contiene le sostituzioni: (var0 , INTvar4STRING STRING), (var2 , INTvar4STRING STRING), (var3 , STRING). Il tipo parziale calcolato è: [uno:Obj_0var2 var2].

Ÿ [uno=@(x)x , due=@(x , y)y]^[due=@(x , y)y].due()

Consideriamo come esempio adesso un’Extract. Come prima cosa definiamo un oggetto il cui corpo è costituito da due metodi identificati dagli ormai soliti selettori

(17)

sua volta due. Il risultato dell’inferenza è un “Error type class”, in quanto due andrebbe chiamato con un parametro.

Ÿ [uno=@(x)x , due=@(x , y)y]^[due=@(x , y)y].due(c)

Questo esempio è uguale al precedente tranne per il fatto che il tipo calcolato adesso è corretto. L’inferenza mi restituisce infatti il tipo var3 che è quello del range di due. La traccia contiene la sostituzione identità (var1 , var3), (var0 , var3) ed il tipo parziale [due:Obj_1var3 var3].

• [m=@(x)x.l() , l=@(x)89 , k=@(x)x.m()]

Questo esempio mostra un aspetto molto interessante del sistema: le chiamate a metodi interni cioè a metodi che sono chiamati e definiti all’interno dello stesso oggetto. In questo caso il metodo identificato dal selettore m invoca sul proprio self (su se stesso) il metodo l definito subito dopo, mentre il metodo k invoca su stesso il metodo m precedentemente citato. Il tipo inferito dallo algoritmo è: [m:Obj_0 INT , l:Obj_0 INT , k:Obj_0 INT]. La traccia contiene le sostituzioni (var2 , INT), (var3 , INT), (var5 , INT), (var4 , INT) e (var6 , INT). Il tipo parziale calcolato è: [m:Obj_0 var2 , l:Obj_0 var3 , k:Obj_0 var4].

• [m=@(x)x.l() , l=@(x)x]

Anche questo esempio mostra una chiamata a metodi interni. L’oggetto contiene due metodi identificati rispettivamente dai selettori m ed l. Il metodo m invoca sul proprio self il metodo l definito succesivamente. Il tipo inferito è: [m:Obj_0 Obj_0 , l:Obj_0 Obj_0]. La traccia contiene le sostituzioni (var1 , Obj_0), (var2 , Obj_0), (var3 , Obj_0). Il tipo parziale calcolato è: [m:Obj_0 var1 , l:Obj_0 var2].

(18)

• [l=@(x)x , m=@(x)x.l()]

E’ un esempio del tutto speculare al precedente, presentato solo per dimostrare che il sistema funziona sia che il metodo venga definito prima (questo caso) o dopo (precedente) della sua definizione. Il tipo inferito è: [m:Obj_0 Obj_0 , l:Obj_0 Obj_0]. La traccia contiene le sostituzioni (var1 , Obj_0), (var2 , Obj_0), (var3 , Obj_0). Il tipo parziale calcolato è: [m:Obj_0 var1 , l:Obj_0 var2].

• [m=@(x)[n=@(x)x.l() , l=@(x)[m=@(x)x.n()]] , k=@(x)x.m().l()]

Altra chiamata di metodi interni (all’oggetto). In questo esempio però, l’oggetto si mostra un po’ più complesso. Ha due metodi identificati dai selettori m e k. Il metodo m ha come unico parametro il self e come corpo un nuovo oggetto con due nuovi metodi n e l. Il metodo con nome n chiama l su se stesso, mentre l ha come corpo l’ennesimo nuovo oggetto che invoca sul proprio self il metodo n appena definito. Infine il metodo identificato da k invoca l scorrendo la struttura di m (invocazione a due livelli di profondità). Il tipo calcolato per l’oggetto è: [m:Obj_2 [n:Obj_1 [m:Obj_0 var10] , l:Obj_1 [m:Obj_0 var10]] , k:Obj_2 [m:Obj_0 var10]].

La traccia contiene le sostituzioni:

(var4 , [n:Obj_1 [m:Obj_0 var10] , l:Obj_1 [m:Obj_0 var10]]), (var6 , [m:Obj_0 var10]), (var7 , [m:Obj_0 var10]), (var8 , [m:Obj_0 var10]), (var9 , var10), (var5 , [m:Obj_0 var10]).

Il tipo parziale calcolato è: [m:Obj_2 var4 , k:Obj_2 var5].

• let c = [l=@(x)x] in c.l()

(19)

classe stessa il metodo in questione. Il tipo calcolato è: Obj_2. La traccia contiene le sostituzioni: (var7 , Obj_2), (var8 , Obj_2).

Il tipo parziale calcolato (cioè il tipo dell’oggetto) è: [l:Obj_2 Obj_2].

• Type A = [l:INT INT , m:STRING var10 , n:REAL REAL] epyT [m=@(x,k)"Name" , n=@(x,h)34.6 , l=@(x,n)45]:A

Ci troviamo di fronte ad una dichiarazione esplicita di tipo. Sulla prima riga tra la keyword d’apertura Type e quella di chiusura epyT è posta la definizione del tipo esplicito della classe a cui è assegnato il nome A. La classe contiene tre metodi identificati rispettivamente dai selettori l, m, n. Il metodo l prende un parametro di tipo INT e restituisce come risultato un INT. Il metodo n prende un parametro di tipo REAL e fornisce come risultato a sua volta un REAL. Il più interessante fra i tre è comunque il metodo identificato da m: il suo dominio è rappresentato da un parametro di tipo STRING mentre non viene specificato il tipo del risultato calcolato. Questo viene indicato specificando al posto di un tipo noto, nel codominio il nome di una variabile di tipo (una sorta di rudimentale polimorfismo, nel nostro esempio var10). Sulla seconda riga è invece indicata la definizione vera e propria della classe: i metodi come ben si nota, sono sempre l, m ed n. In accordo alla dichiarazione esplicita tutti e tre i metodi hanno tra i loro parametri oltre al self anche un altro di cui non è specificato il tipo: toccherà all’inferenza stabilirlo. Notare che alla fine della dichiarazione della classe è indicato esplicitamente il tipo che ad essa si vuole assegnare (A nel nostro caso). Questa volta il tasto da premere nell’Applet per ottenere la valutazione è Analisi. Il risultato che si potrà leggere nel riquadro di valutazione sarà: CORRECT TYPE!

Il tipo inferito per l’oggetto è: [m:Obj_1STRING STRING , n:Obj_1REAL REAL , l:Obj_1INT INT]. L’inferenza, appoggiata ovviamente dall’unificazione, ha stabilito che il tipo del rango (codominio) del metodo m è STRING. Sempre come conseguenza della pressione del tasto di Analisi si potrà leggere quale è la sostituzione sotto cui i due tipi unificano. Nel nostro caso: (var10 , STRING), (var1 ,

(20)

STRING), (var3 , REAL), (var5 , INT). La traccia questa volta mi fornisce la sostituzione: (var0 , STRING), (var2 , REAL), (var4 , INT). Il tipo parziale calcolato è: [m:Obj_1var1 var0 , n:Obj_1var3 var2 , l:Obj_1var5 var4].

• Type A = [l:INT INT , m:STRING var10 , n:REAL REAL] epyT [m=@(x,k)"Name" , n=@(x,h)34.6 , l=@(x,n)45]:B

È un esempio inserito solo per motivi di completezza, per mostrare come si comporti il sistema quando si cerca di assegnare ad una classe un tipo che non è stato esplicitamente dichiarato. In realtà è lo stesso esempio precedente con un’unica, piccola ma significativa modifica: alla definizione della classe viene assegnato il tipo B che però non è stato definito. Alla pressione del tasto Analisi il sistema risponderà: “Errore! Si sta cercando di associare il tipo: B. Tale tipo non è stato dichiarato. Impossibile continuare!“

• Type A = [l:INT INT , m:STRING var10 , n:REAL REAL] epyT [m=@(x,k)"Name" , n=@(x,h)34.6]:A

Esempio molto interessante che mostra il funzionamento della relazione di sottotipo. Diciamo che un tipo tb è un sottotipo di un tipo ta se e solo se ta contiene tutti i selettori di tb. Notiamo come in questo esempio la classe (oggetto) definita sia effettivamente una specializzazione della classe più generale che ha tipo dichiarato esplicitamente sulla prima riga. Infatti possiede solo i metodi identificati dai selettori

n ed m con tipo compatibile. Il tipo inferito per l’oggetto risulta:

[m:Obj_1STRING STRING , n:Obj_1REAL REAL]. La sostituzione sotto cui i due tipi unificano risulta essere: (var10 , STRING), (var1 , STRING), (var3 , REAL). La traccia mi fornisce la sostituzione: (var0 , STRING), (var2 , REAL). Mentre il tipo parziale calcolato risulta essere: [m:Obj_1var1 var0 , n:Obj_1var3 var2].

Riferimenti

Documenti correlati