« Tcl: Iterazioni: una implementazione Tcl del comando doAppunti di Tcl/TkTcl: Creazione di nuovi comandi: comandi global e upvar »

Tcl

creazione di nuovi comandi

comandi proc e return

In Tcl non si fa differenza tra le funzioni e i comandi. La maggior parte dei linguaggi, come il C, hanno una serie di parole riservate, ma in Tcl non vi sono parole riservate. Ad esempio la keyword while è riservata in C, e non può essere utilizzata come identificatore, mentre in Tcl il comando while è solo uno di quei comandi che l'interprete Tcl conosce di default, cioè che si trovano elencati in una tabella che l'interprete usa per analizzare una linea di codice. Volendo la vostra applicazione può ridefinire il comando while assegnandogli un comportamento anche totalmente diverso. Questo naturalmente non è raccomandabile, quindi non fatelo, ma il fatto di poterlo fare rende chiaro che in Tcl non vi è alcuna differenza tra le possibilità dei comandi definiti dall'utente e quelli predefiniti nell'interprete.

Per aggiungere un nuovo comando (detto anche procedura Tcl) all'arsenale dei comandi noti all'interprete si usa il comando proc(n):

proc name args body

Questo comando crea un nuovo comando Tcl di nome name. Internamente quello che fa è di creare una struttura per definire la procedura in memoria e aggiungere la struttura e il nome della procedura alla tabella usata dall'interprete per riconoscere i comandi. Come accennato in Tcl le procedure si possono ridefinire a run-time, ossia se il comando esiste già, allora verrà sostituito dal nuovo comando definito con lo stesso nome.

args è la lista degli argomenti formali che saranno passati alla procedura. Tcl è stato in parte influenzato dal linguaggio Lisp (LISt Processor) e permette di manipolare le stringhe, il suo unico tipo di dati, come delle liste, come vedremo meglio in seguito. Quando il comando name viene invocato, verranno create delle variabili locali al comando, aventi i nomi qui definiti e i valori degli argomenti da passare al comando name saranno copiati in queste variabili locali (oppure la variabile assume il valore di default, vedere sotto). Le variabili locali vengono eliminate dalla memoria quando la procedura ritorna. args può essere la lista vuota, ossia si può definire un comando senza argomenti. Ad esempio potrebbe essere comodo definire un comando help che stampa una stringa d'uso di aiuto per l'utente, qualora questo venga invocato in vari punti del programma:

% proc help {} {puts "Usage: ..."}
% help
Usage: ...

Ogni elemento della lista args specifica un argomento (ricordo ancora una volta che tutte le variabili in Tcl, quindi anche le variabili locali, sono stringhe). Ciascun specificatore di argomento o elemento della lista args è a sua volta una lista che può avere però solo uno o due campi (due stringhe racchiuse tra { }). Se lo specificatore ha un solo elemento, allora questo è considerato il nome dell'argomento; se ci sono due campi, allora il primo è il nome dell'argomento-variabile e il secondo indica il valore di default dell'argomento.

body indica il corpo di codice da eseguire quando viene invocata la procedura name.

Il valore di ritorno di un comando viene definito tramite il comando return(n), che appunto ritorna il suo argomento al programma chiamante. Se il comando return non appare nel body, allora la terminazione del body si avrà naturalmente quando sono esaurite le sue istruzioni, quindi quando viene raggiunta l'ultima parentesi graffa chiusa e la procedura ritorna in tal caso il valore dell'ultimo comando eseguito (questo vi fa risparmiare l'uso dell'istruzione return in alcuni casi). Se volete ritornare la stringa vuota quindi va fatto esplicitamente con un return "" o return {} o semplicemente return senza argomenti. Il valore di ritorno del comando proc(n) stesso invece è la stringa vuota.

Riprendendo un esempio precedente, creiamo un comando che converte un numero intero num in base base, con la base che ha per valore di default 8, se non altrimenti specificato, perché supponiamo che la conversione in base 8 sia quella più frequentemente richiesta:

proc obase {num {base 8}} {
  for {set numb ""} {$num} {set num [expr $num/$base]} {
    set numb [expr $num%$base]$numb
  }
  if {$numb==""} {set numb 0}
  return $numb
}

Esempi d'uso:

% obase
no value given for parameter "num" to "obase"
% obase 10
12
% obase 10 2
1010

Cosa succede se decidessi di scambiare l'ordine in cui sono definiti gli argomenti di obase, in questo modo?

proc obase {{base 8} num} {
  ...
}
% obase
no value given for parameter "num" to "obase"
% obase 10
no value given for parameter "num" to "obase"
% obase 8 10
12
% obase 2 10
1010

Come notate, in questo caso perdiamo i vantaggi degli argomenti di default e siamo sempre costretti a specificare esattamente due argomenti.

In generale infatti è richiesto che vi siano abbastanza argomenti attuali per tutti gli argomenti formali che non hanno valori di default e non possono esserci più argomenti di quelli dichiarati, ad eccezione delle funzioni con numero variabili di argomenti che introdurremo in seguito. Nell'esempio precedente capita che se forniamo un solo argomento, questo viene assegnato a base e poi Tcl genera un errore perché non è stato definito il valore dell'argomento num (che non ha valore di default).

I valori di default non devono essere necessariamente specificati, ma comunque gli argomenti attuali e formali vengono associati nell'ordine in cui sono elencati nel comando (da sinistra verso destra); come regola pratica ponete alla fine della lista degli argomenti il gruppo degli argomenti con valore di default (eventualmente in questo caso l'ultimo argomento subito dopo il gruppo degli argomenti con valore di default può essere args, vedere sotto); nel caso questo gruppo sia formato da più di un argomento con valore di default, allora tenete conto che non c'è modo di utilizzare un valore di default senza specificare tutti i valori di default precedenti.

La possibilità di poter definire delle procedure (nuovi comandi) risulta fondamentale per migliorare lo stile di programmazione. Invece di scrivere un programma complicato come un'unica routine molto lunga e quindi difficile da seguire per un altro programmatore (o voi stessi, specialmente dopo qualche giorno o peggio ancora qualche mese o anno), e anche difficile da modificare, la routine principale dovrebbe essere breve e la maggior parte del lavoro dovrebbe essere suddiviso tra le varie procedure.

Inoltre se definite opportunamente in modo che siano di utilità generica, le procedure favoriscono il riutilizzo del codice, sia nello stesso programma che tra programmi diversi.

Le procedure non dovrebbero mai essere troppo lunghe e dovrebbero essere dedicate ad un task ben preciso. Se una procedura, come risultato di varie modifiche nel tempo, diventa ingestibile perché troppo complicata e lunga, forse potete spezzarla in più procedure, che sono più facili da comprendere e da modificare.

Infatti le procedure, tramite l'interfaccia dei parametri, permettono di controllare meglio le interazioni tra le varie sezioni del codice. Avere molte semplici procedure di cui è facile mostrare la correttezza, dà migliori speranze che l'intero programma sia corretto.

L'argomento chiamato args è speciale: se l'ultimo l'argomento formale si chiama args, allora la chiamata alla procedura può contenere un numero di argomenti attuali maggiore o uguale al numero di argomenti formali. In contrasto alle procedure che abbiamo visto finora, che hanno un numero di argomenti non superiore al numero di argomenti dichiarato, quando viene usato args si possono creare procedure a cui è possibile passare un numero arbitrario di parametri. Anche il C contiene questa funzionalità, ad esempio basti pensare a funzioni quali printf(3), scanf(3) della libreria stdio.

La variabile locale args diventa la lista di tutti gli argomenti attuali che vengono passati, a partire da quello che dovrebbe essere assegnato da solo ad args se args non fosse speciale. In altri termini tutti gli argomenti che non sono stati assegnati a precedenti variabili locali, verranno assegnati ad args. args è sempre del tipo stringa; infatti contiene i valori combinati in una lista di tutti i parametri che gli sono stati assegnati, e la lista è rappresentata tramite una stringa; per risolvere eventuali ambiguità due o più parole che costituiscono un unico valore del parametro sono racchiuse tra { }. Questo è il modo in cui in Tcl si rappresentano le liste, su cui torneremo meglio in seguito.

Ad esempio Tcl mette a disposizione un comando format(n), simile a sprintf(3) del linguaggio C, per generare una stringa formattata. Questo comando internamente viene implementato in C proprio appoggiandosi a sprintf(3) della libreria stdio del C. Ecco come in Tcl stesso potrebbe essere definito il comando format(n), che accetta un numero variabile di argomenti:

proc format {formatString args} {
  ...
}

Esempi d'uso di format(n):

% format %o 10
12
% proc power2table maxexp {
  set power 1
  puts [format "%-2s %10s" # power]
  for {set i 0} {$i<=$maxexp} {incr i} {
    puts [format {%2d %10d} $i $power]
    set power [expr $power*2]
  }
}
% power2table 20
#       power
 0          1
 1          2
 2          4
 3          8
 4         16
 5         32
 6         64
 7        128
 8        256
 9        512
10       1024
11       2048
12       4096
13       8192
14      16384
15      32768
16      65536
17     131072
18     262144
19     524288
20    1048576

Se dopo una variabile argomento con valore di default c'è un'altra variabile senza valore di default, allora se quest'ultima variabile è diversa da args, il valore di default è inutile, in quanto non verrà mai usato, ossia deve comunque essere sempre specificato esplicitamente. Mentre se c'è args, il valore di default può essere utilizzato e non è inutile; quando viene utilizzato, la lista degli argomenti associata ad args sarà vuota.

In Tcl è corretto definire delle procedure all'interno di altre procedure, ma comunque tutti i comandi hanno visibilità globale:

% proc test {} {
    proc test2 {} {
        puts test2
    }

    puts test
    test2
}
% test2
invalid command name "test2"
% test
test
test2
% test2
test2

Notate che se se invocare test2 prima di test ottenete un errore causa comando inesistente, in quanto test2 viene creata e ricreata dopo ciascuna invocazione di test.

Essendo Tcl un linguaggio interpretativo, fornisce delle funzionalità dinamiche, che non sono di facile realizzazione nei linguaggi compilati come il C. Ad esempio esiste un comando rename(n) che permette di rimuovere oppure rinominare un altro comando. Qui è stato usato il termine comando, che non è altro che un sinonimo di procedura:

rename test prova ;# rinomina il comando test in prova
rename test2 "" ;# cancella il comando test2

« Tcl: Iterazioni: una implementazione Tcl del comando doAppunti di Tcl/TkTcl: Creazione di nuovi comandi: comandi global e upvar »