Updated 2005-02-04 15:20:51 by msw

if 0 {

MSW(2005-02-04)

So this is the scenario: You have a User Interface which asks the user to input some parameter for an operation, be it the call of a procedure or of a standalone, external tool via exec, whatever. You likely will want to ensure that the values you are given from the user are in the range of the domain of parameters for the given procedure(s) and/or tool(s). When the user gets something wrong, you will want to inform him of a) that something went wrong, b) what went wrong and c) what he has to do to fix it.

When confronted with this situation in Tk-GUIs, I used to write intelligent dialogs, tons of argument checking for different functions which I knew would be called from it, directly or indirectly, was embedded into the dialog itself. Of course this leads to a nice separation of the original procedure and the checking of their arguments.

So I wondered whether it wouldn't be possible to toast a proc up with some callback info (basically a widget) and a callback to collect argument checking in certain places again, but independant of the source of information from the user, while still allowing to signal him. The code below does exactly that.

}
 namespace eval Toaster {
    variable tokenCtr 0
    namespace export unknown toaster
 }
 ## genToken
 # returns next free token
 proc ::Toaster::genToken {} {
    variable tokenCtr
    return "_proc[incr tokenCtr]"
 }
 ## errWidget
 # std callback on error, signal an event to the widget.
 proc ::Toaster::errWidget {widget} {
    event generate $widget <<Errant>>
 }
 ## toaster
 # toaster wraps up the proc to something callable with a bit
 # more state. A list of vars which has to be congruent with
 # proc's arg list can be passed to associate info with the
 # arguments.
 proc ::Toaster::toaster {proc {vars {}} {callb {Toaster::errWidget}} } {
    set argl [info args $proc]
    set tok [genToken]
    namespace eval [namespace current] [list namespace eval $tok {array set args {}}]
    foreach v $vars a $argl {
        set ${tok}::args($a) $v
    }
    set ${tok}::callb $callb
    set ${tok}::proc $proc
    set ${tok}::errI ""
    set ${tok}::errC 0
    return "${tok}::${proc}"
 }
 ## unknown
 # unknown makes the thingies returned by toaster callable.
 # if it isn't something toasted, let tcl do its work.
 proc ::Toaster::unknown {cmd args} {
    if {[regexp {(_proc\d+)::(.+)} $cmd -> ns prc]} then {
        call $ns $prc $args
    } else {
        uplevel [list ::tcl::unknown $cmd $args]
    }
 }
 ## call
 # call the toasted thingie. If an error occurs, see if some
 # argument was involved which has associated info, if there is
 # one, call the callback which was toasted onto the thingie
 # with that info.
 proc ::Toaster::call {nspace proc argl} {
    if {![string match {*unknown} [lindex [info level [expr {[info level] - 1}]] 0]]} then {
        error {::Toaster::call not called from unknown!}
    }
    set val {}
    # uplevel 2: call->unkown=1, unkown->orig context=2
    if {[catch {uplevel 2 "$proc $argl"} val]} then {
        set ${nspace}::errI $::errorInfo
        set ${nspace}::errC $::errorCode
        # Was one of our watched vars involved ?
        foreach n [array names ${nspace}::args] {
            if {[lindex [strInErr $::errorInfo $n] 0] == 2} then {
                set a $n; set w [set ${nspace}::args($n)]
                [set ${nspace}::callb] $w
            }
        }
        return -code return
    } else {
        return $val
    }
 }
 ## strInErr
 # Checks whether given regex happens to be in the cause of
 # the error passed in err (procedure and 1st level of "while
 #  executing" is checked)
 proc ::Toaster::strInErr {err regex} {
    set v $err
    set pat {.*\s+while executing\s*"(.*)"\s+\(procedure "(\w+)" line (\d+)\)}
    if {![regexp $pat $v -> exec proc line]} then {
        return [list 0]
    }
    if {[regexp $regex $exec]} then {
        set kind 2
        set matched $exec
    } elseif {[regexp $regex $proc]} then {
        set kind 3
        set matched $proc
    } else {
        return [list 1]
    }
    return [list $kind $matched $exec $proc $line]
 }

if 0 {

}
 # test cases
 if {[info script] == $argv0} then {
    # make space for tcl's unknown:
    if {[catch {rename unknown ::tcl::unknown} err]} then {
        puts stderr "Toaster: Cannot rename unkown -> ::tcl::unkown:\n$err"
        exit 1
    }
    # get things toasted:
    namespace import Toaster::*
    ## #1: unknown testing
    # the toasted thingies are callable, aren't they ?
    proc bla {} {
        return "bla!"
    }
    puts "Calling bla: [bla]"
    set func [toaster bla [list]]
    puts "Calling func $func: [$func]"
    ## #2: proc with an arg
    # passing on of arguments works fine, too ?
    proc bla2 {arg} {
        return "bla: $arg!"
    }
    puts "Calling bla2 banzai: [bla2 banzai]"
    set func [toaster bla2 [list]]
    puts "Calling func $func banzai: [$func banzai]"
    ## #3: proc with an arg, triggering callback
    # This time the toasted proc is triggering an
    # error if the argument is not a number or outside
    # the range [0; 3[. When the error occurs, the
    # toasted thing should realize arg #1 is involed
    # in the error message and thus call the callback
    # "callb" with the stored info (when it was toasted)
    proc callb tag {
        puts stderr " ERROR! Tag=$tag"
    }
    proc bla3 {arg} {
        if {![regexp {\d+} $arg]} then {
            error "arg $arg not a number."
        } elseif {$arg < 0 || $arg > 2} then {
            error "arg $arg out of range."
        }
        lindex {banzai ayaken bla} $arg
    }
    puts "Calling bla3 1: [bla3 1]"
    set func [toaster bla3 error-tag callb]
    puts "Calling ${func}(0): [$func 0]"
    puts "Calling ${func}(\"bla\"): [$func bla]"
    puts "Calling ${func}(20): [$func 20]"
    ## #4: now with a widget.
    # when you hit the button, the text from the entry widget below
    # appears in the entry widget above, except when it's a number.
    # Then the callback sends the <<Errant>> event to the widget,
    # which makes it red.
    package require Tk
    proc bla4 {arg} {
        if {[regexp {\d+} $arg]} then {
            error "No numbers allowed, but passed $arg!"
        }
        .t1 delete 0 end
        .t1 insert end $arg
    }
    entry .t1
    .t1 insert end "text shows up here."
    entry .t2
    .t2 insert end "Enter text here."
    button .b -command "[toaster bla4 .t2] \[.t2 get\]" -text "Hit me!"
    pack .t1 .t2 .b
    bind .t2 <<Errant>> {
        .t2 configure -bg red
    }
 }

if 0 {
 # Example test output:
 # Calling bla: bla!
 # Calling func _proc1::bla: bla!
 # Calling bla2 banzai: bla: banzai!
 # Calling func _proc2::bla2 banzai: bla: banzai!
 # Calling bla3 1: ayaken
 # Calling _proc3::bla3(0): banzai
 #  ERROR! Tag=error-tag
 # Calling _proc3::bla3("bla"):
 #  ERROR! Tag=error-tag
 # Calling _proc3::bla3(20):

So what exactly is this good for ? I was thinking of writing wrappers for the functions, which do all of the argument checking, and are pretty clear in the error messages (like the above check for the lindex argument for bla3). You don't have to care how specifically the information will be carried to the user when writing these wrappers, but instead only concentrate on the pre- and post-conditions. All you have to take care of is that the parameter name must appear in the error message.

The callback above is only a sketchy example, something more sophisticated could display the real error (e.g. "Argument must be a number between 0 and 3") as returned from the check-wrapper in some dialog box, while also generating <<Errant>> events to the widget(s) which would guide the User immediately to the errant place(s).

The net effect is that the argument checking proc(s) can be bundled with the procs whose argument they check, and the proc(s) which handle errant user input can be bundled with the gui-implementation.

Caveats:

  • Only sends stuff on errors, when it's successful, should tell the widget the callback was all fine so it can cleanup earlier modifications done as reaction on incoming <<Errant>> events.
  • The name of the proc parameter must appear in the error message, else the callback isn't triggered (doesn't know which data it should select)
  • There's more, I bet...

Category Example }