Procs as objects

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...