« Tcl: Creazione di nuovi comandi: comandi proc e returnAppunti di Tcl/TkTcl: Manipolazione delle stringhe »

Tcl

creazione di nuovi comandi

comandi global e upvar

Finora abbiamo definito, tramite set(n) delle variabili sia all'interno dei comandi che all'esterno. C'è una importante differenza tra queste due alternative.

Le variabili definite all'interno di un comando tramite set(n) vengono create dall'interprete quando viene eseguito il comando, e sono parte di un'area dati corrispondente alla chiamata di un comando che viene inserita in uno stack; quando l'esecuzione del comando termina, lo stack frame ad essa associato viene rimosso dallo stack e quindi non è più possibile riferire queste variabili all'esterno del comando che le ha definite. Si dice che queste hanno visibilità locale (local scope) e vengono anche dette variabili locali:

% proc test {} {
    puts [set x 1]
}
% test
1
% puts $x
can't read "x": no such variable

Le variabili definite all'esterno di un comando tramite set(n) invece permangono dal momento della definizione fino alla fine dell'intero programma (a meno che non siano eliminate prematuramente tramite unset(n)), e si dice che hanno visibilità globale (global scope), in quanto sono accessibili al codice principale, ma anche ai tutti i comandi invocati dopo la loro definizione, previa indicazione da parte di quest'ultimi di voler accedere ad una variabile globale, tramite il comando global(n):

% puts $x
can't read "x": no such variable
% set x 1
1
% puts $x
1
% proc test {} {puts $x}
% test
can't read "x": no such variable
% proc test {} {global x; puts $x}
% test
1

Una o più variabili possono essere tolte prematuramente dall'ambiente globale o dallo stack frame in cui sono inserite tramite l'istruzione unset(n) che provoca appunto la cancellazione anticipata della variabile. La unset(n) può essere quindi usata sia per le variabili globali che per quelle locali; in entrambi i casi l'effetto è che la variabile subito dopo l'unset non esiste più e, a meno che non venga prima ricreata, è un errore riferirsi ad essa.

Lo spazio globale è unico, come in C, mentre quelli locali possono essere molteplici e sono organizzati a stack, con lo spazio globale posto sul fondo.

È possibile avere delle variabili locali aventi lo stesso nome di variabili nella procedura chiamante (o nel codice principale); si tratta di variabili distinte, anche se hanno lo stesso nome: una si trova nella tabella delle variabili globali, mentre l'altra si trova nella tabella delle variabili corrispondente all'attivazione del comando:

% set x 1
1
% proc test {} {puts [set x 2]}
% test
2
% puts $x
1

Il comando global(n) ha la seguente sintassi:

global varname ?varname ...?

Esso viene ignorato se viene eseguito al di fuori del corpo di una procedura, ossia nel codice principale. Se usato all'interno di una procedura invece indica all'interprete che, per la durata della procedura corrente (e solo questa), qualsiasi riferimento alle variabili i cui nomi sono specificati come argomento deve essere risolto nell'ambiente globale, vale a dire che varname deve essere intesa come una variabile globale con lo stesso nome e non come una variabile locale come avviene invece per default.

Questo in realtà viene attuato dall'interprete creando una variabile locale con lo stesso nome di quella globale che è però un riferimento alla variabile globale. In questo modo tutti i riferimenti alle variabili all'interno di una procedura vengono risolti cercando la variabile solo nello stack frame della procedura stessa; global non fa altro che creare dei link in questo stack frame a variabili nell'ambiente globale, che vengono seguiti dall'interprete. Così facendo viene evitato di dover fare una ulteriore ricerca nell'ambiente globale e si aumenta l'efficienza dell'interpretazione.

In un linguaggio compilato come il C invece i riferimenti alle variabili globali vengono risolti a tempo di compilazione e collegamento, ovvero quando si genera l'eseguibile, senza alcun costo di ricerca particolare. Lo specificatore di classe di memoria extern del C è l'analogo del comando global(n) di Tcl, ma mentre in C le dichiarazioni di extern sono opzionali se la variabile globale è definita prima della funzione che la usa, in Tcl global(n) deve essere sempre utilizzato.

global(n) permette quindi di usare le variabili globali proprio come se fossero delle variabili locali e quindi un comando può modificare una variabile globale:

% set x 1
1
% proc test {} {global x; incr x}
% test
2

Le variabili globali sono convenienti per mantenere dei valori che tutto il codice può leggere o modificare, evitando di dover passare delle variabili per copia o per riferimento tra una procedura e l'altra.

Il comando upvar(n) è molto più potente di global(n) in quanto permette di scegliere per il link un nome di variabile differente rispetto alla variabile puntata. Inoltre mentre global(n) permette l'accesso solo alle variabili globali, upvar(n) consente di creare dei collegamenti ad una variabile di un qualsiasi stack frame. Questa è una funzionalità più potente non presente tipicamente nei linguaggi compilati come il C.

upvar(n) ha la sintassi:

upvar ?level? otherVar myVar ?otherVar myVar ...?

Per ciascuna coppia di argomenti otherVar myVar, upvar(n) crea una variabile locale myVar che è un link alla variabile chiamata otherVar nel frame della procedura a livello level.

level indica la distanza della procedura a cui appartiene la variabile di cui si vuole creare il link e la procedura corrente nello stack delle chiamate delle procedure. Il valore di default per level è 1 che indica proprio lo stack frame immediatamente inferiore sullo stack. Se il primo argomento otherVar inizia con una cifra oppure col carattere # (infatti in Tcl il nome di una variabile può anche iniziare con questi caratteri), il valore di default va specificato per evitare la scorretta interpretazione dei parametri. Se level consiste di un carattere # seguito da un numero allora si intende un numero di livello assoluto anziché relativo. Ad esempio #0 indica il livello del main program, qualsiasi sia il numero del livello corrente.

Sussiste la seguente equivalenza:

global x <-> upvar #0 x x

Quindi global(n) non è necessario, in quanto upvar(n) può sostituirlo sempre, ma esiste perché è più facile da usare per riferirsi alle variabili globali, quelle del primo stack frame per intenderci, che sono anche quelle maggiormente richieste.

Un altro esempio che usa il valore di default 1 per il livello:

% proc test {} {
    set i 2
    test2
}
% proc test2 {} {
    puts $i
}
% test
can't read "i": no such variable
% proc test2 {} {
    upvar i i
    puts $i
}
% test
2

upvar(n) può essere utilizzato anche per far sì che una procedura possa modificare il valore dei suoi argomenti. Ad esempio il comando swap definito di seguito scambia i suoi due argomenti e potrebbe essere utile in una procedura di ordinamento basata su confronti e scambi. Esso esemplica la tecnica che si usa in Tcl per simulare il passaggio dei parametri per riferimento alle funzioni come viene fatto nel C e in altri linguaggi compilati; con la differenza che in Tcl, non essendoci puntatori, viene passato proprio il nome della variabile trasmessa per riferimento, che come tutti i valori di Tcl è una stringa. Si indica poi all'interprete, tramite upvar(n), di creare una variabile locale che è un riferimento a quella variabile nel frame immediatamente superiore avente il nome che è stato passato per valore:

proc swap {x y} {
  upvar $x xr $y yr

  set t $yr
  set yr $xr
  set xr $t

  return
}
% set x 1
1
% set y 2
2
% swap x y
% puts "x=$x, y=$y"
x=2, y=1

È stato necessario utilizzare per i link dei nomi diversi da x e y perché upvar(n) richiede che non esista una variabile di nome myVar nel momento in cui viene invocato.

È facile verificare che una versione senza il comando upvar(n) a tutti gli effetti non farebbe alcuno scambio, a causa del fatto che il passaggio avviene per valore:

proc swap {x y} {
  set t $y
  set y $x
  set x $t

  return
}

La funzionalità offerta da upvar(n) di poter legare il nome di una variabile nello scope corrente a quello di una variabile in un qualsiasi altro scope esistente a runtime, va usata con moderazione. Infatti usare upvar(n) con un numero di livello 2 o superiore introduce delle dipendenze tra procedure che ne riduce la riusabilità; si rischia inoltre che una modifica di una procedura influisca su altre, introducendo involontariamente un subdolo errore nel programma. Meglio quindi non abusare troppo della potenza di upvar(n) se non strettamente necessario.

Un discorso analogo vale per le variabili globali. Non è possibile evitarne l'uso, ma bisogna evitare di avere troppe variabili globali. Un programma con troppe variabili globali diventa difficile da capire a meno di non studiarsi nel dettaglio come ogni procedura modifica l'ambiente globale e difficile da modificare pure senza introdure errori. Questa stessa raccomandazione d'altronde vale anche per altri linguaggi che hanno le variabili globali come il C e il Fortran.

Un altro problema con le variabili globali, quando vi è un solo spazio globale, è che codici diversi, ad esempio scritti da programmatori diversi, possono usare una variabile globale con lo stesso nome per scopi differenti. Se poi questi codici devono essere incorporati, allora in uno dei due codici il nome della variabile globale dovrà essere cambiato altrimenti molto probabilmente non funzionerà correttamente l'intero programma. Se ciascuno dei due pacchetti usa per le variabili globali un prefisso unico, ad esempio invece di chiamare una variabile globale maxSize essa si chiama Stack_maxSize o List_maxSize, allora non saranno necessari cambiamenti.

Per ottenere una lista di tutte le variabili globali definite al momento utilizzate il comando info(n), col sottocomando globals. Ad esempio queste sono le variabili globali definite nel momento in cui si avvia l'interprete Tcl (ho diviso l'output su più linee per comodità di lettura):

$ tclsh8.4
% info globals
tcl_rcFileName tcl_version argv argv0 tcl_interactive auto_oldpath errorCode
auto_path errorInfo auto_index env tcl_pkgPath tcl_patchLevel argc tcl_libPath
tcl_library tcl_platform

Per maggiori informazioni sulle variabili globali predefinite di Tcl vedere la manpage tclvars(n).

« Tcl: Creazione di nuovi comandi: comandi proc e returnAppunti di Tcl/TkTcl: Manipolazione delle stringhe »