Richard Suchenwirth 2002-05-21 - Procs, our little helpers, that we call (and most often also write) every day in Tcl, can have interesting twists and turns - see Procs as data structures, which may contain no valid code at all. Here I'm meditating on chapter 3.1.1 in SICP, where rudimentary objects are introduced that have both methods (instructions what to do) and static variables, whose value is retained between calls, on the example of bank accounts. SICP uses the LISP dialect Scheme for its examples, and I very often wonder whether we can do that in Tcl too.
Method dispatching based on an (typically the first) argument is well- known in Tcl, as known from Tk, or info globals, string length, file delete..., and also a frequent style in OO. For static variables, which for procedures can be implemented as default arguments, I like remember best which first appeared in Streams:
proc remember {argn value} { # - rewrite a proc's default arg with given value set procn [lindex [info level -1] 0] ;# caller's name set argl {} foreach arg [info args $procn] { if [info default $procn $arg default] { if {$arg==$argn} {set default $value} lappend argl [list $arg $default] } else { lappend argl $arg } } proc $procn $argl [info body $procn] set value }
# That's all we need to get started:
proc make-account {id balance} { proc $id [list cmd amount [list balance $balance]] { switch -- $cmd { deposit {remember balance [expr {$balance + $amount}]} withdraw { if {$balance < $amount} { error "insufficient funds" } else { remember balance [expr {$balance - $amount}] } } default {error "unknown request $cmd"} } } set id ;# just in case the caller wants to re-use it }
if 0 {As in SICP, we return the account ID; but in contrast, we also require it as an input parameter (otherwise an unique account numbering scheme, with a global or static variable, would have to have been included too). But this is no big limitation, if you look at the example use cases:
% make-account W1 100 W1 % make-account W2 100 W2 % W1 withdraw 50 50 % W2 withdraw 70 30 % W2 withdraw 40 insufficient funds % W2 deposit 40 70 % W1 withdraw 33 17
As in SICP, the independence and persistence of the accounts' balances is demonstrated. Better don't base a bank on this code, however, because it can be too easily cheated by overriding the balance:
% W1 withdraw 33 9999999 9999966
Hence, although this code shows how bare-bone objects (no inheritance and stuff, just methods and static variables) can be had by just writing a proc that writes a proc (which rewrites itself when state changes ;-), "private variables" that are protected from meddling from outside are less Tcl's cup of tea than transparency, openness and power...
In the above implementation, each account has its own proc. Assume we have many accounts, whose methods may be updated over time - then it's more economic (in memory) and robust to have the code just in one place, and redirect the account instances to it with an alias that also keeps the static variable(s): }
proc make-account2 {id balance} { if [llength [info commands $id]] { error "can't override command $id" } setBalance $id $balance } proc setBalance {id balance} { interp alias {} $id {} _account $id $balance set balance } ##-- the generic account exists only once, namely here: proc _account {id balance cmd amount} { switch -- $cmd { deposit {setBalance $id [expr {$balance+$amount}]} withdraw { if {$balance < $amount} { error "insufficient funds" } else { setBalance $id [expr {$balance - $amount}] } } default {error "unknown request $cmd"} } }
Now maintenance of account policy (e.g., allowing limited overdraw, paying interest, or charging fees) can be done in one place, the "class code" for accounts, while the static data (the balance) is kept only in the alias definition - not very "private" either, and cheating is again possible by changing an alias, or calling setBalance directly...
See also subproc, Tiny OO with Jim and Jim closures - oh I wish Tcl had closures...