Progetto

L'obiettivo del progetto è scrivere un interprete per una versione ridotta del linguaggio Self.

Self è stato sviluppato da David Ungar e Randall Smith nel 1986.

La motivazione principale della creazione di tale linguaggio è proporre un linguaggio orientato agli oggetti molto semplice.
La nozione centrale di Self è quella di oggetto. Non ci sono classi, i.e. tutte le operazioni si fanno al livello di oggetto. Un oggetto è definito come

(| Definizione dei campi separati da "."| Sequenza di espressioni separate da "." )
All'esecuzione di tale oggetto, prima si crea l'oggetto con i suoi campi, poi viene valutata la sequenza di espressioni. Il risultato dell'esecuzione è il valore restituito dall'ultima espressione. Quando la sequenza è vuota, si restituisce l'oggetto stesso. Per esempio, la valutazione di
(| x = 2. y = 3 | x + y)
restituisce l'oggetto che rappresenta l'intero 5. Invece, la valutazione di
(| x = 2. y = 3 |)
restituisce un oggetto con due campi, x che contiene 2, e y che contiene 3.

L'oggetto vuoto si rappresenta come (||) o (). La valutazione di

(1+2)
crea l'oggetto vuoto e restituisce 3.

Inizializzando un campo con x = valore, si crea un campo x in sola lettura il cui valore è accessibile con il metodo x.

Inizializzando un campo con x <- valore, si crea un campo modificabile, il cui valore è accessibile con il metodo x e che può essere modificato dal metodo x:. Per esempio, la valutazione di

(| x <- 2 | x: (x + 2). x)
restituisce 4.

Un campo non inizializzato corrisponde ad un campo modificabile inizializzato con l'oggetto nil. Dunque

(| x. y |)
è equivalente a
(| x <- nil. y <- nil |)
In Self, ci sono solamente oggetti, dunque nil, true, false, 0, 1, ... sono tutti oggetti.

Dentro un campo si può mettere qualsiasi oggetto. Per esempio, la valutazione di

(| point = (| x <- 1. y <- 2 |) | point x: 2. point x)
restituisce 2: si crea un oggetto con un campo point che contiene un oggetto con due campi inizializzati x e y, la prima espressione aggiorna il campo x del campo point con 2, la seconda espressione restituisce il campo x del campo point.

Metodi

I metodi sono campi che contengono oggetti con codice. Per esempio, la valutazione di
(| x <- 1. y <- 2. sum = (x + y) | x: 3. sum)
restituisce 5. Si crea un oggetto con un campo x che contiene 1, un campo y che contiene 2 e un metodo sum senza parametri che restituisce la somma del campo x e del campo y. Il codice associato si valuta nel modo seguente: la prima espressione aggiorna il campo x, la seconda espressione chiama il metodo sum.

Un metodo con un argumento è rappresentato da un campo il cui nome ha il suffisso :. L'oggetto contenuto in questo campo ha esattamente un campo il cui nome inizia con :. Per esempio, la valutazione di

(| x <- 1. incr: = (| :n | x: (x + n)) | incr 2. incr 4. x)
restituisce 7. Si crea un oggetto con un campo x inizializzato con 1 e un metodo incr: che prende un argomento n e incrementa il contenuto del campo x con n.

Le operazioni sui numeri non seguono questa convenzione di denominazione dei metodi. Invece di scrivere 1 add: 2, i.e. il metodo add applicato a 1 con argomento 2, si scrive più semplicemente 1 + 2.

La convenzione di usare il suffisso : per il nome di un metodo con un solo argomento spiega quello che accade quando si crea un campo modificabile. La valutazione di (| x <- 1 |) crea infatti un oggetto con un campo x e un metodo x: che prende un solo argomento e permette di aggiornare il campo.

Un metodo può anche avere altri argomenti non prefissati con il simbolo : per simulare variabili locali. Per esempio, la valutazione di

(| x <- 1. y <- 2. sum = (| res <- 0 | res: (res + x). res: (res + y). res) | sum)
restituisce 3. Si crea un oggetto con due campi e un metodo senza argomenti che utilizza una variabile locale res per accumulare i valori contenuti nei campi x e y.

Per definire metodi con più di un argomento si utilizzano nomi di metodi con più parole che permettono di separare i diversi argomenti. Ogni parola ha come suffisso il simbolo :, la prima inizia con una minuscola, le altre con una maiuscola. Per esempio, la valutazione di

(| add:With:And: = (|:x :y :z| x + y + z) | add: 1 With: 2 And: 3)
restituisce 6. Crea un oggetto con un metodo che prende 3 argomenti. Nel codice, si utilizza questo metodo per calcolare 1+2+3.

Self

Ogni oggetto ha un metodo di default self che restituisce se stesso. Dunque
(| x <- 1. y <- 2|)
è equivalente a
(| x. y | x: 1. y: 2. self)
L'applicazione di un metodo su self è implicita. Dunque
(| x <- 1, y <- 2, sum = (x + y) | sum)
è equivalente a
(| x <- 1, y <- 2, sum = (x + y) | self sum)
Il valore restituito dall'aggiornamento di un campo restituisce self. Dunque, la valutazione di
(| point = (| x <- 1. y <- 2 |) | (point x: 2) y: 3. self)
restituisce un oggetto con un campo point che contiene un oggetto che ha due campi: x che contiene il valore 2, y che contiene il valore 3. Il codice si esegue come (((self point) x: 2) y: 3): il risultato dell'aggiornamento point x: 2, i.e. self, è l'oggetto sul quale è chiamato il secondo aggiornamento.

Ereditarietà

L'ereditarietà è gestita in Self dai campi il cui nome ha il suffisso *. Un tale campo contiene l'oggetto che è esteso. L'ereditarietà multipla si fa tramite più campi con il suffisso *. Per esempio, la valutazione di
(| x1 = (| x <- 1 |) .
   x2 = (| y <- 2 |) .
   x3 = (| parent1* . parent2* . sum = (x + y) |)
 | x3 parent1: x1.
   x3 parent2: x2.
   x3 sum)
restituisce l'intero 3. Si crea un oggetto con tre campi: Il codice associa al primo campo d'ereditarietà l'oggetto contenuto dentro x1, al secondo associa l'oggetto contenuto dentro x2. Dunque, dopo le prime due istruzioni, l'oggetto contenuto dentro il campo x3 eredita dai due oggetti contenuti dentro i campi x1 e x2. Quindi, nell'esecuzione del metodo sum, quando si richiede il campo x è quello di x1 che è restituito; quando si richiede il campo y è quello di x2 che è restituito.

Nota che il codice seguente non è valido

(| x1 = (| x <- 1 |) .
   x2 = (| y <- 2 |) .
   x3 = (| parent1* <- x1 . parent2* <- x2 . sum = (x + y) |)
Non si può usare un campo di un oggetto fino a quando la definizione dell'oggetto non è completa.

La regola per selezionare campi e metodi è la seguente. Si guarda nell'oggetto se tale campo o metodo esiste, altrimenti si cerca ricorsivamente negli oggetti contenuti nei campi d'ereditarietà (quelli con il suffisso *). Se esiste un'unica soluzione, questa viene selezionata. Se non esiste soluzione o esiste più di una soluzione, l'esecuzione è abortita con un messaggio d'errore. Per esempio, l'esecuzione di

(| x1 = (| x <- 1 |) .
   x2 = (| x <- 2 |) .
   x = (| parents1* . parents2* |)
 | x3 parents1: x1.
   x3 parents2: x2.
   x3 x)
genera un messaggio che indica che chiamare x su x3 è ambiguo. Nota che quando si eredita un campo o un metodo, non si cambia il valore di self. Dunque, la valutazione di
(| x1 = (| sum = (x + y) |) .
   x2 = (| parents*. x <- 1. y <- 2 |)
 | x2 parents: x1.
   x2 sum
 )
restituisce 3. La chiamata del metodo sum sul contenuto di x2 trova il suo codice dentro l'oggetto contenuto dentro x1 ma poiché, per l'esecuzione del corpo del metodo sum, self è sempre l'oggetto contenuto dentro x2, i campi x e y sono visibili.

Resend

Si può usare il codice di un metodo sovrascritto (l'equivalente del super di Java) usando la parola chiave resend. Per esempio, la valutazione di
(| x1 = (| x <- 1 |) .
   x2 = (| parents* . x = (resend.x + 2) |)
 | x2 parents: x1.
   x2 x
)
restituisce 3. La sovrascrittura del campo x incrementa il campo ereditato di 2.

Si può anche dare il campo d'ereditarietà esplicitamente. Per esempio, la valutazione di

(| x1 = (| x <- 1 |) .
   x2 = (| x <- 2 |) .
   x3 = (| parents1* . parents2*. x = (parents1.x + parents2.x |)
 | x3 parents1: x1.
   x3 parents2: x2.
   x3 x
)
restituisce 3. La sovrascrittura del campo x fa la somma dei due campi ereditati.

Chiusura

Una chiusura permette di passare codice come argomento a un metodo. Per questo, si usa una notazione simile a quella dei metodi usando le parentesi quadre [] invece di quelle rotonde (). Per attivare una chiusura, i.e l'equivalente della chiamata di metodo, si chiama il metodo value. Se la chiusura ha un argomento, si chiama il metodo value:, il metodo value: With: per due argomenti, il metodo value: With: With: per 3, etc... Per esempio, la valutazione di
(| x <- 1. action: = (| :a | x: (a value:  x))
 | action: [| :n | n + 2].
   action: [| :n | n + 4].
   x
)
restituisce 7. Il metodo action è chiamato con due chiusure diverse, la prima incrementa di 2, la seconda di 4.

Una proprietà fondamentale della chiusura è che la sua valutazione si fa nello stesso contesto (il self) che quello della sua definizione. Per esempio, la valutazione del codice

(| x1 = (| x <- 3. action: = (| :n | n + n). eval: = (| :a | a value: x) |).
   x <- 2.
   action: = (| :n | n * n). 
 | x1 eval: [| :m | action: m]
)
restituisce 9. Il metodo action che è chiamato è quello della creazione della chiusura, non quello dell'oggetto contenuto dentro x1 che attiva la chiusura.

La chiusura permette di implementare le strutture di controllo. Per esempio, il condizionale è implementato da un metodo ifTrue: False: sui booleani che prende due chiusure. La prima indica che sarà eseguita se il booleano è vero, la seconda se il booleano è falso. Ciò significa che l'implementazione di questo metodo per l'oggetto true è

 ifTrue: False: = (| :codeT . :codeF | codeT value)
l'implementazione di questo metodo per l'oggetto false è invece
ifTrue: False: = (| :codeT . :codeF | codeF value)
Usando il condizionale, si può definire l'iterazione. Per esempio, la valutazione di
 (| 
    res <- 0. 
    from:To:Do: =
        (| :start :end :action |
             (start > end) ifTrue: [] False: [action value: start.
                                                 from: (start + 1)
                                                 To: end
                                                 Do: action])
  | from: 1 To: 10 Do [| :i | res: (res + i)].
    res
 )
restituisce 55, i.e la somma dei numeri da 1 a 10.

Lobby

Il lobby rappresenta l'insieme degli oggetti che sono accessibili da tutti. Il lobby iniziale contiene l'oggetto nil, i due booleani true e false e i numeri 0,1,2,..

Su tutti gli oggetti si possono applicare due metodi:

Sui booleani, il metodo == è l'uguaglianza di valore e il metodo not restituisce il complemento.

Sui numeri, il metodo == è l'uguaglianza di valore e i metodi +,-,*,<=,< permettono le operazioni usuali sui numeri.

Si può aggiungere nuovi elementi al lobby. Usando questa possibilità, si può riscrivere il codice seguente

(| x1 = (| sum = (x + y) |) .
   x2 = (| parents*. x <- 1. y <- 2 |)
 | x2 parents1: x1.
   x2 sum
 )
in modo più naturale. Una prima valutazione
  x1 := (| sum = (x + y) |)
dove la valutazione di (| sum = (x + y) |) è associata con il nome x1 al lobby. Adesso, la valutazione di
 (| parents* = x1. x <- 1. y <- 2 | sum)
restituisce 2.

Progetto

La base del progetto consiste nell'avere una rappresentazione di un programma Self, i.e. una sequenza di valutazioni come un oggetto Java. Per esempio,

  x1 := (| sum = (x + y) |);
 (| parents* = x1. x <- 1. y <- 2 | sum)
potrebbe corrispondere all'oggetto
new Program(
  new Command[]{
    new AddLobby("x1",
        new Object(
          new Slots[]{
            new Method("sum", new Expr[]{},
                       new Call("+",
                                new Call("x", new Self(), new Expr[]{}),
                                new Expr[]{
                                 new Call("y", new Self(), new Expr[]{}),}))
          },
          new EmptyCode())),
    new Object(
      new Slots[]{
        new Pointer("parents",
                      new Call("x1", new Self(), new Expr[]{})),
        new Field("x", new Int(1)),
        new Field("y", new Int(2))
      },
      new Call("sum", new Self(), new Expr[]{}))
  })
La base deve permettere di rappresentare tutti gli esempi presentati in questa pagina.

La base del progetto dovrà obbligatoriamente essere consegnata prima della prova scritta dell'esame.

A partire da questa base, si può completare il progetto realizzando per esempio:

Lo studente dovrebbe seguire le convenzioni di codifica date durante il corso. Per verificare la conformità del progetto, si può usare l'archivio checkstyle.jar. Eseguendo

java -jar checkstyle.jar File.java
si genera un file report che indica la conformità del codice.

Informazioni supplementari

Il primo articolo che descrive il linguaggio Self si trova qui. Un'introduzione completa si trova qui. Un'implementazione per Windows e Linux si trova qui. Per qualsiasi dubbio sul progetto, si può contattarmi qui.
Laurent Théry
Last modified: Sun Jan 25 03:52:36 MET 2004