L'argomento trattato in questa sezione è ripreso
e discusso più in generale alla pagina "Estensione
dell'interprete". Qui si tratta l'argomento con riferimento specifico
al caso Lisp.
L'interprete nasce con l'obiettivo di essere facilmente
espandibile. A tal proposito descriviamo quali sono i passi da seguire
se si vuole aggiungere una primitiva all'interprete. Scegliamo la solita
primitiva let (che comunque è già presente nell'interprete)
e vediamo qual'è il procedimento.
Le possibilità sono 2: modifica del codice
esistente, oppure aggiunta di nuovo codice. La prima possibilità
è lecita se si ha a disposizione il codice stesso, altrimenti è
necessario ricorrere alla seconda possibilità.
Modifica del codice (modularità).
In questo caso il compito è estemamente facile. Infatti la nuova
primitiva determinerà una nuova ConsSexp la cui testa è il
nome della primitiva. Quindi la valutazione avviene valutando una ConsSexp. La
valutazione di una ConsSexp in cui la testa sia il nome di una primitiva
avviene chiamando il metodo applyPrimitive(). In questo metodo è
presente una catena di if che determinano il nome della primitiva ed eseguono
il codice corrispondente. Quindi è sufficiente far sì che
l'interprete riconosca come tale la primitiva che si vuole aggiungere (nel
nostro caso la let ) ed abbia nel metodo applyPrimitive() un if dedicato
ad essa. In conclusione è sufficiente seguire le seguenti istruzioni:
il Lexer non deve essere modificato a meno che non si voglia denotare la
nuova primitiva con un simbolo "particolarmente strano";
bisogna modificare la classe PrimitiveSet aggiungendo l'istruzione
add("let"), così che l'interprete riconosca la stringa "let" come
una primitiva di nome let;
il Parser non deve essere modificato (creerà una ConsSexp);
infine si deve aggiungere la valutazione nella EvalSexpVisitor (il visitor
per la valutazione), in particolare nel metodo applyPrimitive();
Aggiunta di nuovo codice(estensibilità
incrementale). Se non si vuole modificare il codice originale,
oppure non lo si ha a disposizione si può comunque seguire questa
via per aggiungere primitive. Il compito è ugualmente semplice.
Infatti se si aggiunge la nuova primitiva al PrimitiveSet (in questo caso
lo si farà tramite l'apposito metodo setPrimitive(nomePrimitiva))
l'interprete la riconoscerà e quindi l'unico problema che resterà
sarà quello di aggiungere la valutazione. Questa aggiunta dovrebbe
avvenire definendo una sottoclasse dell'EvalSexpVisitor esistente che aggiunga
un metodo visit dedicato alla classe che si crea per la nuova primitiva.
Però nel nostro caso non si crea una nuova classe, ma si usa sempre
ConsSexp. Allora la sottoclasse dell'EvalSexpVisitor deve ridefinire il
metodo applyPrimitive() e aggiungere in esso il codice che riconoscerà
tramite un if la nuova primitiva e la eseguirà. Prima di mettere
quell'if dovrà essere effettuata la chiamata super.applyPrimitive()
e controllato il valore restituito. Il valore restituito è un boolean
e vale true se la primitiva è stata valutata (quindi si trovava
nel set di primitiva iniziali), false se non è stata valutata. Se
quindi il valore restituito è false allora significa che quella
primitiva è una primitiva aggiunta, ovvero è probabilmente
la let. Allora a questo punto si può catturarla con l'if. Se non
si tratta nemmeno della let allora si dovrà restituire false, poiché
evidentemente qualcuno ha definito in un secondo momento una nuova primitiva
con lo stesso peocedimento. Il metodo che ha chiamato applyPrimitive()
controllerà che il valore di ritorno sia true, altrimenti genererà
un'eccezione indicante che si è aggiunta una primitiva al PrimitiveSet
ma non è stato definito il codice per la sua valutazione: "<nomeprimitiva>
is added to PrimitiveSet, but his evaluation's code is not implemented".
Vediamo ora di riassumere il procedimento:
si deve definire una sottoclasse della classe EvalSexpVisitor;
questa nuova classe dovrà avere un costruttore ed il metodo applyPrimitive();
il costruttore riceverà come parametro un oggetto della classe Parser
ed invocherà il metodo setPrimitive(String) di quell'oggetto, passandogli
la stringa corrispondente al nome della nuova primitiva (ovvero "let" nel
caso in esempio): in questo modo la nuova primitiva è stata aggiunta
all set di primitive e quindi verrà automaticamente riconosciuta
dal lexer e dal parser;
il metodo applyPrimitive() dovrà avere il seguente codice:
if (super.ApplyPrimitive()) return true;
if (nuovaprimitiva){gestione primitiva; return true};
return false;
Note:
un esempio di estensione è riportato nel package Extension in cui
è stato introdotto la primitiva "sqr" (elevamento al quadrato):
si veda il file ExtendedLispEvalSexpVisitor.java oppure nella pagina
dedicata all'estensione dell'interprete;
se si vuole modificare (o eliminare) una primitiva esistente bisogna catturarla
prima di super:
boolean applyPrimitive(primitiva, argomenti){
if (primitiva è primitivaDaCambiare){gestione primitiva; return
true};
if (super.ApplyPrimitive(primitiva, argomenti)) return true;
if (primitiva è nuovaprimitiva){gestione primitiva; return true};
return false;
}
questi passi per l'aggiunta di codice (derivazione della classe ed aggiunta
di un if con return true o false) corrispondono esattamente al comportamento
della AWT 1.0 per la gestione della HandleEvent
una possibilità alternativa era quella di aggiungere alla classe
PrimitiveToken (è la classe a cui appartengono gli oggetti costruiti
con le stringhe corrispondenti ai nome delle primitive) il metodo eval()
che avrebbe semplicemente generato un'eccezione. Dalla applyPrimitive()
se non si poteva valutare la primitiva in in questione si sarebbe richiamato
il metodo eval() della primitiva sconosciuta passandogli gli argomenti
e l'environment locale (oppure tutto il visitor). In questo modo per imlementare
una nuova primitiva si sarebbe dovuto creare una sottoclasse della PrimitiveToken
per la nuova primitiva, contenente il codice per la valutazione nel metodo
eval().