[incr Tcl]

« Primo incontro con classi e oggettiAppunti di Tcl/TkEsempio: la classe LinkedList »

Esempio: la classe pack

Nel paragrafo precedente abbiamo visto come un primo aspetto della programmazione ad oggetti è che introduce un concetto di tipo record (struttura) esteso (la classe), che ha funzionalità addizionali quali il controllo delle inizializzazioni e la protezione dalla manipolazione diretta dei dati e delle funzioni che non fanno parte dell'interfaccia.

Abbiamo anche discusso le due ragioni fondamentali per cui queste funzionalità sono utili: primo riducono l'accoppiamento tra i vari moduli che costituiscono il nostro software, permettendo di apportare cambiamenti alla struttura e alle operazioni interne di un modulo senza che tutti gli altri ne siano influenzati e debbano essere modificati e secondo aumentano la sicurezza nella programmazione, rendendo molto più difficile per il programmatore del codice cliente introdurre inconsistenze nelle strutture dati incapsulate.

Per prendere confidenza con questo primo ma importante aspetto del paradigma OO e vedere qualche altra funzionalità offerta da itcl in riferimento all'incapsulazione, affrontiamo adesso il progetto di una classe un po' più complicata. Vogliamo creare un tipo astratto per rappresentare e manipolare un mazzo di carte francesi. Questa classe potrebbe essere parte di un programma più complicato che implementa un qualche gioco di carte.

Come noto ogni carta ha un seme (cuori, fiori, quadri, picche) e un valore. La scala dei valori è la seguente: due, tre, quattro, cinque, sei, sette, otto, nove, dieci, fante, regina, re, asso.

Per incapsulare le informazioni relative a semi e valori e fornire un comando per iterare su queste sequenze nell'ordine canonico, ho creato due classi simili, chiamate suit e value:

# file: suit.itcl

class suit {
  common suits [list hearts clubs diamonds spades]

  proc foreach {varname body}
}

body suit::foreach {varname body} {
  ::foreach suit $suits {
    upvar 1 $varname loopvar
    set loopvar $suit
    uplevel 1 $body
  }
}
# file: value.itcl

class value {
  common values [
    list two three four five six seven eight nine ten jack queen king ace
  ]

  proc foreach {varname body}
}

body value::foreach {varname body} {
  ::foreach value $values {
    upvar 1 $varname loopvar
    set loopvar $value
    uplevel 1 $body
  }
}

La serie dei simboli e dei valori sono fissate una volta per tutte. Le ho rappresentate tramite delle liste. Quando si vogliono rappresentare in [incr Tcl] delle variabili comuni a tutti gli oggetti di una classe, al posto dello statement variable si usa il common, che ha la sintassi:

common varName ?init?

Quando viene definita la classe suit all'interno dello spazio nomi globale, viene creato anche un namespace figlio di quello globale, il cui nome pienamente qualificato è ::suit. Questo namespace conterrà le variabili di classe (in altri linguaggi dette anche statiche):

% namespace children
::auto_mkindex_parser ::itcl ::suit ::pkg ::tcl

La differenza tra le variabili definite con variable e quelle definite tramite common è che delle prime esiste una istanza distinta dalle altre in ciascun oggetto della classe, mentre delle seconde esiste una sola istanza, indipendentemente dal fatto che esistano o meno oggetti della classe, e tale istanza è condivisa tra tutti gli (eventuali) oggetti della classe.

Attualmente Tcl non offre alcun controllo per limitare l'accesso a variabili, comandi o altri namespace definiti all'interno di un namespace. È buona norma quindi, all'esterno della classe, non accedere direttamente a variabili comuni private, nonostante questo purtroppo sia sempre possibile. Come si può accedervi allora? Analogamente a come si fa con le variabili di oggetto (quelle definite tramite variable), utilizzate piuttosto i metodi e le procedure dell'oggetto anche per manipolare i campi common, a meno che questi non siano pubblici. L'esempio seguente mostra come avviene l'accesso diretto sia in lettura che in scrittura al campo condiviso suits di suit, che invece dovrebbe essere protetto (ricordate che i campi dati, sia condivisi che non, per default sono protetti):

% puts $::suit::suits
hearts clubs diamonds spades

% set ::suit::suits garbage
garbage

% puts $::suit::suits
garbage

Per creare un mazzo di carte occorre combinare ciascun seme con ciascun valore. Abbiamo quindi bisogno di iterare su tutti i possibili semi (4) e su tutti i possibili valori (13), creare una carta con quel seme e quel valore e aggiungerla al mazzo. In totale le carte sono 13 * 4 = 52.

Una soluzione potrebbe essere quella di fornire una procedura di classe che ritorna i valori del campo comune suits (e analogamente per values). Si può puoi utilizzare il comando predefinito foreach(n) per iterare su tutti gli elementi di queste liste.

Tuttavia esiste un modo migliore, che fa sì che il codice esterno non dipenda dal fatto che l'elenco dei semi e dei valori è implementato tramite una lista (piuttosto che ad es. un array) e permette di cambiarne la rappresentazione senza che il codice esterno ne sia influenzato.

Questa è la soluzione che è stata adottata e consiste nel definire nella classe suit (e analogamente in value) una procedura di classe che, per indicare l'analogia con l'istruzione predefinita foreach, ho deciso di chiamare proprio foreach (la scelta del nome è libera). Questa procedura ha la stessa sintassi del comando predefinito foreach (per semplicità limitatamente al caso in cui l'iterazione avviene su una sola lista e con una sola variabile):

foreach varname list body

Come si fa distinguere tra i due se hanno lo stesso nome? La procedura foreach ridefinisce il comando predefinito foreach? La risposta è no. Non v'è alcun problema di conflitto di nomi, in quanto il comando predefinito foreach risiede nel namespace globale (::foreach), mentre la procedura foreach viene definita all'interno del namespace della classe a cui appartiene (::suit::foreach, ::value::foreach).

All'esterno della classe suit, quando voglio chiamare la procedura di classe utilizzerò il nome pienamente qualificato ::suit::foreach (oppure suit::foreach), mentre se voglio chiamare il comando predefinito foreach(n) utilizzo ::foreach (oppure semplicemente foreach). Se il nome del comando non è pienamente qualificato (non inizia con ::) Tcl lo cerca prima nel namespace corrente e poi, se non lo trova lì, lo cerca nello spazio nomi globale. Per maggiori dettagli sui namespace Tcl vedere la manpage namespace(n), in particolare la sezione NAME RESOLUTION.

Notate come all'interno del metodo statico foreach, se voglio utilizzare il comando predefinito globale foreach, devo scrivere ::foreach. Se scrivessi semplicemente foreach, avrei tentato di chiamare ricorsivamente lo stesso metodo statico piuttosto che il comando globale foreach.

La sintassi per definire una procedura di una classe è analoga a quella per definirne i metodi:

proc name ?args? ?body?

Le procedure di classe sono come delle procedure ordinarie, ma sono definite all'intero dello spazio nomi della classe ed hanno automaticamente accesso ai soli membri dati comuni della classe (senza bisogno del comando variable(n) o global(n)).

La differenza tra metodi e procedure di classe è che i metodi possono accedere sia alle variabili membro che a quelle comuni, mentre le procedure hanno accesso solo ai dati membri comuni. Questo perchè le procedure non sono invocate come sottocomandi di un oggetto ma proprio come comandi e quindi non si riferiscono ad uno specifico oggetto di invocazione.

Un'altra novità è costituita dall'uso del comando body(n). [incr Tcl], come Tcl, è un linguaggio dinamico. Mentre una classe può essere definita una volta sola (se provate a ridefinire in uno stesso namespace una classe esistente otterrete un errore), i corpi dei suoi metodi e procedure possono essere ridefiniti, esattamente come i comandi Tcl standard. Questa funzionalità serve per due scopi: supportare il debugging interattivo ed aiutare a tenere separate le definizioni dell'interfaccia e dell'implementazione di una classe.

Il comando body va usato al di fuori della definizione di una class con la seguente sintassi:

body className::function args body

Esso definisce per la prima volta oppure ridefinisce il corpo del metodo o procedura function della classe className. La lista degli argomenti args deve corrispondere a quella specificata quando function è stata definita nella definizione della classe. I nomi delle variabili possono cambiare, ma gli argomenti richiesti e i valori di default per argomenti opzionali devono essere gli stessi.

Notate ad esempio come il corpo della procedura foreach nella definizione della classe suit non è stato specificato e viene poi definito per la prima volta nella parte di implementazione tramite un comando body.

Per supportare questa definizione ritardata nella sintassi di definizioni dei metodi e delle procedure di una classe l'argomento body è stato reso opzionale:

method name ?args? ?body?
proc name ?args? ?body?

Per quanto riguarda il costruttore invece la sintassi è:

constructor args ?init? body

Qui notate che il body non è stato reso opzionale. Se lo fosse, non ci sarebbe stato modo di distinguere, nel caso in cui gli argomenti di constructor siano solo due invece di tre, se il secondo argomento è il codice di inizializzazione oppure è il corpo del costruttore. Vedremo in seguito a cosa serve il codice di inizializzazione.

Come si può fare allora se non si vuole definire il corpo del costruttore nell'interfaccia, in modo da distinguere nettamente interfaccia e implementazione? La soluzione consiste nell'utilizzare una ridefinizione: nell'interfaccia (all'interno della classe) il corpo del costruttore, che è obbligatorio, lo si definisce come vuoto e poi nell'implementazione (all'esterno della classe) si utilizza un comando body per ridefinirlo:

# interfaccia
class TestClass {
  constructor {args} {}

  # altri elementi della classe
  # ...
}

# implementazione
body TestClass::constructor {args} {
  # codice del costruttore
  # ...
}

La parte di interfaccia di una classe è quindi costituita da un comando class contenente le dichiarazioni di metodi, procedure, variabili d'istanza e comuni. Un programmatore che vuole utilizzare la classe senza entrare nel dettaglio del suo funzionamento interno, può limitarsi a consultare questa sezione. L'uso dell'interfaccia diventa molto più chiaro se ci sono dei commenti prima di ciascun metodo o procedura che spiegano cosa fa e cosa devono essere i suoi parametri. La parte di implementazione è invece un costituita da una serie di comandi body e configbody. Nel nostro esempio ogni classe viene scritta in un file separato, che contiene sia la parte di interfaccia che l'implementazione relativa. Suddividendo ancora, si potrebbe addirittura tenere ogni implementazione in un file separato rispetto alla relativa interfaccia. Questo consente di ricaricare solo il file dell'implementazione, ogni volta che viene corretto qualche baco o raffinata l'implementazione, in modo da supportare lo sviluppo interattivo.

configbody viene utilizzato per definire o ridefinire al di fuori di una classe il codice di configurazione associato alle variabili pubbliche, secondo la seguente sintassi:

configbody className::varName body

Poichè le classi suit e value non contengono metodi o variabili pubbliche ad eccezione di quelli predefiniti, non serve a molto creare delle istanze, anche se rimane comunque possibile farlo:

% suit s
s

% s info function
::suit::foreach ::suit::isa ::suit::configure ::suit::cget

% s info variable
::suit::suits ::suit::this

In realtà faremo uso delle classi suit e value, chiamando le loro procedure di classi, piuttosto che istanziare singoli oggetti:

% suit::foreach suit {puts $suit}
hearts
clubs
diamonds
spades

Per rappresentare una carta useremo la seguente classe card:

class card {
  variable suit
  variable value

  # Creates and initializes a new card object and returns its name.
  constructor {suitval valueval} {}

  method identifysuit {}

  method identifyvalue {}

  method display {}
}

body card::constructor {suitval valueval} {
  set suit $suitval
  set value $valueval
}

body card::identifysuit {} {
  return $suit
}

body card::identifyvalue {} {
  return $value
}

body card::display {} {
  puts "$value of $suit"
}

Una carta ha un seme e un valore, che si trasformano in variabili di istanza. Queste variabili sono private: vanno inizializzate durante la costruzione dell'oggetto e non possono essere più modificate, ma solo lette durante il ciclo di vita dell'oggetto tramite i metodi identifysuit e identifyvalue. Metodi che restituiscono il valore di singole variabili membro private (o protette) sono spesso chiamati metodi "get". Notate che non c'è alcun modo dall'esterno della classe card di modificare il seme e il valore di una carta. L'unico modo è distruggere l'oggetto carta e crearne un'altro con il seme e il valore voluto. Oggetti il cui stato si definisce solo all'atto della costruzione e rimane poi immutato durante il loro uso, fino a quando non vengono cancellati, vengono detti oggetti immutabili oppure a sola-lettura (read-only).

Notate che il costruttore ha due parametri. La forma più generale di costruttore che consiglio è del tipo:

constructor {arg1 .. argn args} {
  # processa arg1 .. argn

  eval configure $args
  # check dei parametri configurabili
}

arg1 .. argn sono n parametri obbligatori read-only. args rappresenta la lista dei parametri con nomi (opzionali), che sono del tipo read-write. Occorre poi che definiate opportuni metodi accessori per leggere il valore di ciascun parametro arg1 .. argn. Credo che questo tipo di definizione sia abbastanza consistente con la sintassi Tcl e nel contempo semplice da gestire.

Dopo aver testato la classe Card, possiamo scrivere la classe che rappresenta il mazzo di carte. Dobbiamo scegliere come rappresentiamo l'insieme delle carte. In Tcl puro abbiamo due possibilità: lista o array. La seguente implementazione usa una lista. Grazie all'incapsulazione se volete variare la rappresentazione utilizzando ad esempio un array potete farlo senza che il codice che utilizza pack debba essere modificato.

class pack {
  variable cards {}

  # Maximum number of cards in a pack.
  common PACK_MAXCARDS 52
  
  # Creates and initializes a new pack object and returns its name.
  constructor {} {}

  # Frees the memory associated with a pack.
  # The object name is removed after this operation.
  destructor {}

  # Places the cards in the pack in a random order.
  method shuffle {}

  # Removes a card from the pack.
  # Returns "" if the pack is empty.
  method deal {}

  # Replaces a card in the pack.
  method replace {card}

  method display {}
}

body pack::constructor {} {
  suit::foreach s {
    value::foreach v {
      replace [card #auto $s $v]
    }
  }
}

body pack::destructor {} {
  foreach card $cards {
    delete object $card
  }
}

# Based closely on 'The Harpist's card shuffle.
body pack::shuffle {} {
  for {set n [expr {[llength $cards] - 1}]} {$n >= 0} {incr n -1} {
    # Random number in the range [0,n]
    set swappos [expr {round(rand() * $n)}]

    set tempcard [lindex $cards $n]
    set cards [lreplace $cards $n $n [lindex $cards $swappos]]
    set cards [lreplace $cards $swappos $swappos $tempcard]
  }
}

body pack::deal {} {
  set card [lindex $cards end]
  set cards [lreplace $cards end end]
  return [namespace which $card]
}

body pack::replace {card} {
  if {[llength $cards] == $PACK_MAXCARDS} {
    error "There will be too many cards"
  }
  lappend cards $card
}

body pack::display {} {
  foreach card $cards {
    $card display
  }
}

La variabile membro cards è una lista (inizialmente vuota) che contiene i nomi degli oggetti di classe card che costituiscono il mazzo. Si tratta di una specie di puntatori letterali agli oggetti. Il metodo replace consente di inserire una carta alla fine del mazzo; può trattarsi di una nuova carta quando viene invocata dal costruttore durante la costruzione del mazzo, oppure di una carta che era stata estratta precedentemente tramite il metodo deal. Notate che la common variable PACK_MAXCARDS indica il numero massimo di carte che ciascun mazzo può avere; se si tenta di inserire in un mazzo un numero di carte maggiore di 52, il metodo replace solleva un errore e il mazzo rimane invariato.

Nel metodo shuffle che mescola le carte utilizzo un algoritmo molto semplice: per ogni carta, a partire dall'ultima fino alla prima, scelgo a caso una carta (sia prima che dopo la carta considerata, al limite anche coincidente) e scambio le due carte. Avrei potuto utilizzare la funzione lset(n) invece di lreplace(n), ma poichè la versione 8.3 di Tcl non ha questa funzione, ho preferito mantenere la retrocompatibilità.

In Tcl ci sono solo puntatori letterali. Questo alle volte può essere un problema in quanto ogni volta che bisogna creare un nuovo oggetto occorre scegliere un nome univoco: creare infatti un oggetto con lo stesso nome di uno già esistente (nello stesso namespace) è un errore. Nel nostro caso dobbiamo creare una lista di (puntatori ad) oggetti ed abbiamo bisogno di un meccanismo di generazione automatica dei nomi. Per fortuna questo problema è facilmente risolvibile in pratica: un buon metodo consiste nell'utilizzare il nome della classe (ma convertiamo l'iniziale in minuscolo per aderire alla convenzione Tcl che i nomi di variabile iniziano in minuscolo) seguito da un certo numero di sequenza.

Quest'operazione è così comune che [incr Tcl] prevede un meccanismo per la generazione automatica del nome, che vi evita di sporcare la definizione di una classe (come card) con l'introduzione esplicita di una variabile di sequenza (e relativo metodo che ne ritorna il prossimo valore) e semplifica la sintassi. Se il nome di un oggetto da creare objName con la sintassi già conosciuta:

className objName ?args...?
contiene la stringa #auto, questa stringa viene sostituita con un nome generato automaticamente del tipo className<number>, dove className è il nome della classe con l'iniziale minuscola e number assumerà valori interi positivi (a partire da 0). Nel nostro caso la lista cards del primo oggetto pack avrà dopo la costruzione il seguente valore: card0 card1 ... card50 card51

Faccio notare che questo metodo non garantisce l'unicità e quindi che non si verifichino errori all'atto della creazione di un oggetto se per caso esiste già un comando o un altro oggetto con il nome generato nello stesso namespace. La generazione di un nome sicuramente unico così pure come il riuso dei numeri di sequenza sarebbe in pratica troppo costosa.

In pratica è raro che si verifichino problemi di collisioni in questo modo, in quanto è raro che un programmatore scelga nomi di oggetti che corrispondono al pattern className<number>. Se necessario potete anche aggiungere un prefisso e/o suffisso per complicare il nome dell'oggetto e rendere ancora più improbabile il rischio di collisioni:

% card pointer#auto hearts ace
pointercard0

% card #autoXYZ clubs queen
card1XYZ

% card auto_#auto_var diamonds five
auto_card2_var

Come abbiamo visto, in molti casi già subito dopo la creazione di un oggetto di una certa classe, vogliamo imporre che lo stato iniziale di questo oggetto sia coerente. Del resto anche nella programmazione tradizionale, è sempre importante inizializzare opportunamente le strutture dati prima di utilizzare delle funzioni che li manipolano.

Quando si crea un oggetto, [incr Tcl] alloca lo spazio di memoria necessario per le variabili membro e le inizializza con una eventule stringa di inizializzazione specificata nella definizione tramite lo statement variable. Questo in alcuni casi può essere sufficiente, ma in altri abbiamo bisogno di una logica di inizializzazione più complessa.

Nella programmazione tradizionale in questi casi si è soliti creare delle funzioni apposite che gestiscono l'inizializzazione di diverse strutture dati dello stesso tipo in modo parametrico. Il corrispondente di queste funzioni nel nuovo paradigma è il costruttore.

Perchè il costruttore non è semplicemente un metodo come tutti gli altri? Un motivo è che per semplificare la sintassi non si richiede che l'utilizzatore della classe chiami esplicitamente il costruttore, quindi bisogna avere un modo per identificare il metodo costruttore come speciale, in modo che l'interprete possa chiamarlo automaticamente ogni volta che viene creato un oggetto. Un altro motivo è che vogliamo che il costruttore ritorni sempre il nome dell'oggetto creato (oppure un errore), e che qualsiasi valore differente eventualmente ritornato sia ignorato. Non è infatti necessario che il costruttore ritorni al chiamante una informazione più articolata: quello che vuole il chiamante è semplicemente sapere se l'oggetto è stato creato o meno. Vedremo in seguito altri motivi, legati al concetto di ereditarietà, che rendono in qualche modo speciale il metodo costruttore.

Un problema duale a quello della corretta costruzione degli oggetti è quello del loro rilascio o distruzione, anche questo certamente non nuovo della OOP. In alcuni casi è sufficiente deallocare lo spazio di memoria che accoglie le variabili membro, cosa che viene fatta automaticamente da [incr Tcl] quando si cancella un oggetto tramite delete object, ma a volte questo non basta. In questi casi si può programmare la logica di distruzione tramite il metodo speciale destructor:

destructor body

Notate che un distruttore è speciale in quanto è privo di argomenti, viene invocato automaticamente quando viene cancellato un oggetto tramite il comando delete object e non ritorna nulla, a meno che la distruzione non fallisca. In tal caso viene ritornato un messaggio di errore e l'oggetto rimane allocato. Quando la distruzione di un oggetto va a buon fine, dopo la corretta terminazione del distruttore, [incr Tcl] si fa carico di rimuovere i dati dell'oggetto e il comando avente lo stesso nome dell'oggetto dall'interprete.

Il distruttore deve solo occuparsi di distruggere altre risorse associate all'oggetto di cui sarebbe troppo costoso e difficile per [incr Tcl] tenere conto dell'appartenenza all'oggetto, come ad esempio altri oggetti contenuti dentro l'oggetto corrente (tramite puntatori letterali), canali di I/O aperti, widget Tk, ecc. in modo che non rimangano inutilizzate per tutta la durata del programma.

Ad esempio un mazzo è composto da un insieme di carte. Se si distrugge il mazzo, occorre distruggere anche le carte, che altrimenti rimarrebbero inutilizzate. Di questo si occupa il distruttore della classe pack.

Se non definite alcun distruttore anche se necessario, il vostro programma continuerà a funzionare correttamente ma potrebbe utilizzare quantità di memoria aggiuntive anche notevoli che invece potrebbero essere liberate a vantaggio di altri processi concorrenti nel sistema. Se un programma non libera la memoria che non gli serve più e gira per lungo tempo, è probabile che la quantità di memoria che richiede divenga nel tempo spropositata, producendo uno spreco delle risorse di sistema o raggiungendo alcuni limiti prestabiliti. Questo potrebbe anche portare ad un crash del programma o addirittura di un intero sistema. Per questo motivo è buona norma definire sempre i distruttori nelle classi per liberare risorse associate ad un oggetto che non servono più quando questo viene distrutto.

Tcl mette a disposizione una funzionalità di auto-loading, che permette di caricare a runtime librerie contenenti sia definizioni di classi [incr Tcl] che normali comandi Tcl, on-demand, ovvero nel momento in cui si utilizza uno dei loro comandi.

Ogni volta che uno script tenta di invocare un comando che non esiste, l'interprete Tcl passa l'intero comando (compresi i suoi argomenti), al comando predefinito unknown(n). Questo chiama un'altra procedura di libreria di Tcl, auto_load(n). auto_load tenta di caricare la definizione del comando. La prima volta che auto_load viene chiamata cerca nelle directory definite dalla variabile globale $auto_path (una lista di nomi di directory). All'interno di ciascuna di queste directory deve essere presente un file chiamato tclIndex, che viene generato tramite il comando auto_mkindex(n). tclIndex contiene un commento iniziale e alcune semplici istruzioni set(n) che aggiungono elementi all'array associativo globale auto_index, come ad esempio (limitatamente alla classe card):

set auto_index(card) [list source [file join $dir card.itcl]]
set auto_index(::card::constructor) [list source [file join $dir card.itcl]]
set auto_index(::card::identifysuit) [list source [file join $dir card.itcl]]
set auto_index(::card::identifyvalue) [list source [file join $dir card.itcl]]
set auto_index(::card::display) [list source [file join $dir card.itcl]]

Una chiave è il nome di un comando definito nella directory in cui si trova tclIndex, mentre il valore è uno script Tcl che tramite source(n) valuta lo script che definisce il comando.

Una volta caricati nell'array auto_index tutti i file tclIndex, auto_load cerca il comando da eseguire come chiave di questo array e se esiste, valuta lo script corrispondente in modo che questo venga caricato in memoria e ritorna 1 ad unknown, il quale esegue il comando con i suoi argomenti originali e ritorna lo stesso valore che ha ritornato il nuovo comando. Se invece il comando non esiste, auto_load ritorna 0 ad unknown, il quale ritorna un errore.

auto_load legge le informazioni di indice dei comandi dai file tclIndex una sola volta (la prima volta che viene chiamata) e le salva tutte nell'array auto_index. Successive chiamate di auto_load cercheranno i comandi da caricare in questo array piuttosto che rileggere i file di indice.

Naturalmente le cose sono più complicate rispetto a come le ho descritte, comunque il corso degli eventi rimane a grandi linee quello descritto. Ad esempio unknown deve anche cercare di espandare i nomi abbreviati dei comandi nel nome completo (se si può fare univocamente) e supportare l'esecuzione di comandi sconosciuti come sotto-processi (questo vi permette ad esempio di usare il comando Unix ls(1) nella shell tclsh). Vi rimando alle manpage (tra cui anche class(n)) se siete interessati ai dettagli.

Dalla descrizione precedente del meccanismo di auto-loading, si evince che ogni volta che aggiungete un nuovo comando (o classe) ad una libreria dovrete rigenerare il file tclIndex chiamando il comando di libreria predefinito in Tcl:

auto_mkindex dir pattern pattern ...

Qui dir è la directory che volete indicizzare (. per la directory corrente). auto_mkindex cerca in dir tutti i file che hanno un nome che corrisponda ad uno degli argomenti pattern e genera l'indice di tutti i comandi Tcl definiti in tutti i file individuati nel file dir/tclIndex. Se non viene specificato alcun pattern si assume per default *.tcl. Prima di chiamare auto_mkindex per indicizzare classi, assicuratevi che l'estensione Itcl sia caricata:

package require Itcl

Inoltre se utilizzate i comandi aggiuntivi di [incr Tcl] (come class) senza pienamente qualificarli (ovvero non scrivete itcl::class), importateli prima di chiamare auto_mkindex:

namespace import ::itcl::*

Se le librerie si trovano in locazioni non standard, il codice che intende utilizzarle dovrà aggiungere le directory in cui si trovano gli indici alla variabile auto_path prima di invocarne i comandi, utilizzando lappend(n) come segue:

lappend auto_path dir dir ...

Archivio completo del codice di questo paragrafo.

Qui trovate anche un main che utilizza l'auto-loading per caricare il codice delle classi presentate in questo paragrafo. Il main crea un mazzo di carte inizialmente ordinato p, ne estrae l'ultima cartae la visualizza (ace of spades, asso di picche), poi la rimette nel mazzo, visualizza l'intero mazzo ordinato. Poi mischia casualmente le carte, visualizza l'intero mazzo dopo la mischia e infine distrugge l'oggetto p e termina:

pack p

set dealtcard [p deal]
puts "-- last card is:"
$dealtcard display

p replace $dealtcard

puts "\n-- ordered pack:"
p display

p shuffle
puts "\n-- shuffled pack:"
p display

delete object p

« Primo incontro con classi e oggettiAppunti di Tcl/TkEsempio: la classe LinkedList »