itins

Incr Tcl Is Not Snit

Sarnold 2005-07-11: It is an object-oriented package built on the top of incr Tcl, but based upon delegation like snit does. It processes some type (or widget) body to build the body an incr Tcl class, and then saves it into a file, or creates the class.

2006/03/05 - Itins is officially abandoned in preference of Xoins. So there would not be updates anymore.

Here is the source of an older version (0.1):

        package require Itcl

    namespace eval itins {
        # export public commands
        namespace export type delete widget wset
        # global variables representing the current type's structure
        variable methods
        variable variables
        variable procs
        variable special
        variable nonDelegatedOptions
        variable unknowns
        variable delegatedOptions
        variable oncfg
        variable onget
        variable isawidget
        variable widgetHandle
        variable className


        proc type {name body {filename ""}} {
            _type $name $body $filename ""
        }

        proc _type {name body filename widget} {
            variable className
            cleanUp $name $widget
            # evaluate the body in current context
            # to perform preprocessing
            namespace eval ::itins::eval $body
            set body "\n[classBody]"
            # store the class into a file,
            if {$filename!=""} {
                set fd [open $filename w]
                puts $fd [list itcl::class $className $body]
                close $fd
                return
            }
            # or use it just now (beware of uplevel because, if it wasn't here,
            # we would get a class inside the itins namespace !)
            itcl::class ::$className $body
            return $className
        }

        proc widget {name body {filename ""}} {
            package require Tk
            _type $name $body $filename widget
        }

        proc wset {class path args} {
            set evaluate "$class [string range $path 1 end]"
            foreach a $args {
                set evaluate [concat $evaluate [::itins::lone $a]]
            }
            [eval $evaluate] hull
            return $path
        }

        proc deleteWidget {path} {
            catch {itins::delete [string range $path 1 end]}
        }

        proc cleanUp {cName {widget ""}} {
            # clean up
            variable className
            set className $cName
            catch {itcl::delete class $className}
            # initializing arrays
            foreach varName {methods delegatedOptions procs unknowns
                oncfg onget special widgetHandle} {
                variable $varName
                array unset $varName
                array set $varName {}
            }
            # and now initializing simple values
            foreach varName {
                nonDelegatedOptions
                variables options typevars typearys} {
                variable $varName
                set $varName ""
            }
            variable isawidget
            set isawidget [expr {$widget eq "widget"}]
            return
        }

        # process variables-related code generation
        proc variables {} {
            variable variables
            variable typevars
            variable typearys
            set result "# variables (instance and common)\n"
            set allvars [concat [keys $variables] [keys $typevars] [keys $typearys]]
            # to check that there are no multiple definition
            findDupKeys $allvars variable
            foreach {name default} $variables {
                append result "private variable $name $default\n"
            }
            foreach {name default} $typevars {
                append result "private common $name $default\n"
            }
            foreach {name default} $typearys {
                append result "private common $name\narray set $name $default"
            }
            return $result
        }

        # generate code related to construction and destruction of object
        proc specials {} {
            variable special
            if {![info exists special(constructor)]} {
                error "no instance constructor"
            }
            if {![info exists special(destructor)]} {
                error "no instance destructor"
            }
            set cons $special(constructor)
            set result "constructor [lone [lindex $cons 0]] [widgetcons [lindex $cons 1]]\n"
            append result "destructor [widgetdestr $special(destructor)]\n"

            return $result
        }

        # the widget constructor
        proc widgetcons {mainConstructor} {
            variable isawidget
            if {!$isawidget} {return [list $mainConstructor]}
            variable widgetHandle
            set result "set hull .\[lindex \[split \$this ::\] end\]\n"
            append result "[getonce widgetHandle type frame] \$hull\n"
            append result "${mainConstructor}\n"
            append result "bind \$hull <Destroy> \{itins::deleteWidget %W\}\n"
            return [list $result]
        }

        # the widget destructor
        proc widgetdestr {mainDestructor} {
            variable isawidget
            if {!$isawidget} {return [list $mainDestructor]}
            set result "${mainDestructor}\n"
            append result "catch {destroy \$hull}"
            return [list $result]
        }


        # build procs bodies
        proc procs {} {
            variable procs
            set result "# proc definitions\n"
            foreach name [array names procs] {
                append result "proc $name [lone [lindex $procs($name) 0]]"
                append result " \{[lindex $procs($name) 1]\}\n"
            }
            return $result
        }

        # build the methods bodies
        proc methods {} {
            variable methods
            set result "# methods definitions\n"
            foreach name [array names methods] {
                foreach {arglist body} $methods($name) {break}
                append result "public method $name [lone $arglist] \{$body\}\n"
            }
            return $result
        }

        proc checkAllOptions {} {
            variable delegatedOptions
            variable nonDelegatedOptions
            set allOpts [keys $nonDelegatedOptions 3]
            foreach target [array names delegatedOptions] {
                lappend allOpts [keys $delegatedOptions($target)]
            }
            findDupKeys $allOpts "option"
        }

        # build the onconfigure and oncget special methods
        proc options {} {
            checkAllOptions
            variable nonDelegatedOptions
            variable delegatedOptions
            set cfgbody "# configure body\npublic method configure [lone args] \{\n"
            append cfgbody "if \{\[llength \$args\]==1\} \{\n"
            append cfgbody "set args \[lindex \$args 0\]\n\}\n"

            append cfgbody "foreach {option value} \$args \{\n"
            set cgetbody "# cget body\npublic method cget [lone args] \{\n"
            append cgetbody "set result {}\nforeach option \$args \{\n"
            # build the delegation for all delegated options
            delegatedOptionBody cfgbody cgetbody
            set optdef "# options\n"
            nonDelegatedOptionBody cfgbody cgetbody optdef
            defaultOption cfgbody cgetbody
            set closing "\}\n\}\n"
            append cgetbody "${closing}return \$result\}\n"
            append cfgbody "${closing}\}\n"
            return "${optdef}\n${cfgbody}\n${cgetbody}\n"
        }

        proc nonDelegatedOptionBody {cfgbodyVar cgetbodyVar optdefVar} {
            upvar $cfgbodyVar cfgbody
            upvar $cgetbodyVar cgetbody
            upvar $optdefVar optdef
            set stmt "switch -exact -- \$option \{\n"
            append cfgbody $stmt
            append cgetbody $stmt
            variable nonDelegatedOptions
            foreach {option default readonly} $nonDelegatedOptions {
                set name [string range $option 1 end]
                append optdef "public variable $name $default\n"
                append cfgbody "$option \{[cfgbody $option $readonly]\}\n"
                append cgetbody "$option \{[cgetbody $option]\}\n"
            }
            return
        }

        proc delegatedOptionBody {cfgbodyVar cgetbodyVar} {
            upvar $cfgbodyVar cfgbody
            upvar $cgetbodyVar cgetbody
            variable delegatedOptions
            foreach target [array names delegatedOptions] {
                foreach {optlist newformlist} [pairs $delegatedOptions($target)] {break}
                # if {[set index [lsearch <optlist> $option]]>=0}
                # {<target> configure [lindex <newformlist> $index] $value
                # return}
                #
                set stmt "if \{\[set index \[lsearch -exact [list $optlist] \$option\]\]>=0\} \{\n"
                append cfgbody $stmt
                append cfgbody "\$$target configure \[lindex [list $newformlist] \$index\] \$value\n"
                append cfgbody "continue\n\}\n"
                append cgetbody $stmt
                append cgetbody "lappend result \[\$$target cget \[lindex [list $newformlist] \$index\]\]\n"
                append cgetbody "continue\n\}\n"

            }
        }

        proc defaultOption {cfgbodyVar cgetbodyVar} {
            upvar $cfgbodyVar cfgbody
            upvar $cgetbodyVar cgetbody
            variable unknowns
            if {[info exists unknowns(options)]} {
                append cfgbody "default \{\$$unknowns(options) configure \$option \$value\ncontinue\}\n"
                append cgetbody "default \{lappend result \[\$$unknowns(options) cget \$option\]\ncontinue\}\n"
            } else  {
                set dontKnow "default \{error \"unknown option '\$option'\"\}\n"
                append cfgbody $dontKnow
                append cgetbody $dontKnow
            }
            return
        }

        proc cfgbody {option readonly} {
            variable oncfg
            set body ""
            if {[info exists oncfg($option)]} {
                if {$readonly} {
                    error "can't configure readonly option '$option'"
                }
                set cfg $oncfg($option)
                # replace $value by the local varname
                append body [string map [list value [lindex $cfg 0]] [lindex $cfg 1]]\n
            }
            if {$readonly} {
                append body "error \"this option is read-only\""
            }
            # the variable hanging to an option
            set name [string range $option 1 end]
            append body "set $name \$value"
            return $body
        }

        # build the 'cget' method body
        proc cgetbody {option} {
            variable onget
            set body ""
            if {[info exists onget($option)]} {
                append body "[string map {result __result} $onget($option)]\n"
            }
            # the variable hanging to an option
            set name [string range $option 1 end]
            append body "lappend result \$$name"
            return $body
        }

        # build the class-body (for [incr Tcl])
        proc classBody {} {
            set result [variables]
            # constructor & destructor
            append result [specials]
            append result [procs]
            append result [methods]
            append result [options]
            return $result
        }


        proc delegateMethod {args} {
            set method [lindex $args 0]
            set args [lrange $args 1 end]
            if {$method eq "*"} {
                # not yet implemented : delegate method * to <target>
                error "not yet implemented : delegate method * to ..."
                foreach {to target} $args {
                    variable unknowns
                    # set a target for delegating every (unknown) proc
                    set unkowns(method) $target
                    return
                }
            } else  {
                # but yet implemented : delegate method <name> to <target>
                foreach {to target} $args {break}
                set newform $method
                catch {foreach {as newform} \
                            [set args [lrange $args 2 end]] {break}}
                namespace eval ::itins::eval [list method $method {args} "eval \$$target $newform \$args"]
            }
        }

        proc delegateOption {args} {
            set option [lindex $args 0]
            set args [lrange $args 1 end]
            if {[string equal $option *]} {
                variable unknowns
                foreach {to target} $args {break}
                testSet unknowns options $target "target for unknown options already defined"
                return
            }
            validateOption $option
            foreach {to target} $args {break}
            set newform $option
            catch {foreach {as newform} \
                        [set args [lrange $args 2 end]] {break}}
            variable delegatedOptions
            validateOption $newform
            AryLappend delegatedOptions $target $option $newform
        }

        proc validateOption {option} {
            if {[string index $option 0] != "-"} {
                error "options should begin by a dash"
            }
            if {![string is alnum [string range $option 1 end]]} {
                error "options should be alpha-numeric"
            }
        }



        # utility procs
        proc AryLappend {arrayName key args} {
            upvar $arrayName arrayVar
            if {![info exists arrayVar($key)]} {
                set arrayVar($key) [lindex $args 0]
                set args [lrange $args 1 end]
            }
            foreach {value} $args {
                lappend arrayVar($key) $value
            }
            return
        }

        proc testSet {arrayName key value errMsg} {
            upvar $arrayName arrayVar
            if {[info exists arrayVar($key)]} {
                error $errMsg
            }
            set arrayVar($key) $value
        }

        # create an arglist, avoiding the 'one-argument' mismatch
        proc lone {arglist} {
            if {[llength $arglist]!=1} {return [list $arglist]}
            return "\{$arglist\}"
        }

        # setonce : if already set, put an error
        proc setonce {var value {errmsg "internal error"}} {
            if {[catch {upvar $var a}]} {
                uplevel set $var $value
            } else  {
                error $errmsg
            }
            return
        }

        # getonce : get the value of the variable if it exists, otherwise return a default value
        proc getonce {var args} {
            if {[uplevel array exists $var]} {
                upvar $var table
                set key [lindex $args 0]
                set default [lindex $args 1]
                if {[info exists table($key)]} {
                    return $table($key)
                }
                return $default
            }
            catch {upvar $var a}
            if {![info exists a]} {
                return [lindex $args 0]
            }
            return $a
        }

        # check whether there are duplicated keys
        proc findDupKeys {keys type} {
            foreach my $keys {
                if {[llength [lsearch -all $keys]]>1} {
                    error "$type $my defined twice"
                }
            }
        }



        # returns keys in a pair-list : {key1 value1 key2 value2 ...}
        # the pair-list can be any tuple, provided $by is set to the
        # number of elements in the tuple
        proc keys {pairs {by 2}} {
            set result {}
            for {set i 0} {$i<[llength $pairs]} {incr i $by} {
                lappend result [lindex $pairs $i]
            }
            return $result
        }

        # return a list of the keys and a list of associated values
        proc pairs {list} {
            set odd ""
            set even ""
            foreach {key val} $list {
                lappend odd $key
                lappend even $val
            }
            return [list $odd $even]
        }

    }

    # commands that can be invoked in the itins::type body
    namespace eval itins::eval {
        ::proc constructor {arglist body} {
            itins::testSet itins::special constructor [list $arglist $body] "constructor redefined"
        }

        ::proc destructor {body} {
            itins::testSet itins::special destructor $body "destructor redefined"
        }


        ::proc method {name args body} {
            itins::testSet itins::methods $name [list $args $body] "method redefined"
        }

        ::proc proc {name args body} {
            itins::testSet itins::procs $name [list $args $body] "proc redefined"
        }

        ::proc delegate {args} {
            switch -- [lindex $args 0] {
                method {eval itins::delegateMethod [lrange $args 1 end]}
                option {eval itins::delegateOption [lrange $args 1 end]}
                default {error "can delegate only methods or options"}
            }
        }

        ::proc typevariable {name args} {
            if {[string equal [lindex $args 0] -array]} {
                lappend itins::typearys $name [lindex $args 1]
                return
            }
            lappend itins::typevars $name [lindex $args 0]
        }
        ::proc variable {name {default ""}} {
            lappend itins::variables $name $default
        }

        # syntax : hull frame, hull toplevel
        ::proc hull {{cmd frame}} {
            if {!$::itins::isawidget} {
                error "hull command does not apply to a non-widget"
            }
            variable hull
            # PLEASE !!! DON'T MODIFY THIS !!! (there is some magic in it)
            # don't do that if you don't want to live the quoting hell
            method hull {} {
                rename $hull ::${hull}:cmd
                ::proc ::$hull {subcmd args} [string map [list %PATH% $this] {
                    return [eval [linsert $args 0 %PATH% $subcmd]]
                }]
                return $hull
            }
            itins::testSet itins::widgetHandle type $cmd "hull type already defined"
        }




        ::proc typeconstructor {body} {
            proc typeconstructor {} $body
            itins::testSet itins::specials typeconstructor yes "typeconstructor redefined"
        }
        ::proc option {name args} {
            itins::validateOption $name
            if {[llength $args]==0} {
                # noop
            } elseif {[llength $args]==1} {
                set default [lindex $args 0]
            } else  {
                foreach {option value} $args {
                    switch -exact -- $option {
                        -default {itins::setonce default $value "-default option repeated"}
                        -readonly   {itins::setonce readonly $value "-readonly option repeated"}
                    }
                }
            }
            lappend itins::nonDelegatedOptions $name [itins::getonce default ""] \
                    [itins::getonce readonly no]
        }

        ::proc onconfigure {option arg body} {
            itins::validateOption $option
            itins::testSet itins::oncfg $option [list $arg $body] "onconfigure already defined for this option"
        }

        ::proc oncget {option body} {
            itins::validateOption $option
            itins::testSet itins::onget $option $body "oncget already defined for this option"
        }
    }
    # public aliases
    interp alias {} itins::delete {} itcl::delete object
    interp alias {} itins::scope {} itcl::scope
    interp alias {} itins::local {} itcl::local

    package provide itins 0.1

An example :

 package require itins


    itins::type Person {
        typevariable nbPersons 0
        variable name
        variable tool
        option -decorate no
        delegate option -setting to tool
        constructor {{myname "Steve McQueen"}} {
            set name $myname
            set tool [Tool #auto]
            incr nbPersons
            puts "Person named : '$name' created"
        }
        destructor {
            itins::delete $tool
            incr nbPersons -1
            puts "Person named : '$name' deleted"
        }
        delegate method tell to tool
        delegate method singing to tool as {sing "Queen"}
        method print {} {
            set msg "My name is $name, and I am "
            if {!$decorate} {
                append msg "not "
            }
            append msg "decorated.\nMy tools settings are [cget -setting]."
            return $msg
        }
        method try {look} {
            return "I am trying to look at $look."
        }
        method whenBusy {{overclock no}} {
            set msg [print]
            append msg "\nWhen I am busy, I do "
            if {!$overclock} {
                append msg "not "
            }
            return "${msg}overclock my CPU."
        }
        proc getNb {} {
            return $nbPersons
        }
        #test procs!
        proc Hello {what} {
            return "Hello $what!"
        }
    } Person.itcl
    source Person.itcl

    itins::type Tool {
        option -setting "select"
        constructor {} {}
        destructor {}
        onconfigure -setting {value} {
            puts "Tool setting!"
        }
        oncget -setting {
            puts "Tool get settings!"
        }
        method tell {message} {
            return "Tool is telling you : '$message'"
        }
        method sing {who what} {
            return "$who sings : '$what'"
        }
    }

 set t [Person #auto "Steve McQueen"]
 puts [$t tell "This is the truth : I am a liar"]
 puts [$t singing "We will rock you!"]
 itins::delete $t

A simple widget example :

    package require itins
    itins::widget Button {
        hull
        variable button
        option -packpad 10
        onconfigure -packpad {value} {
            pack configure $button -padx $value -pady $value
        }
        delegate option * to button

        constructor {args} {
            set button [button $hull.b -text "Click me"]
            pack $button -padx 10 -pady 10 -in $hull
            eval configure $args
        }
        destructor {
            catch {destroy $button}
        }
    }
    proc try {firstname name} {
        global btn
        tk_messageBox -message "Have you ever tried to look like $firstname $name?"
        destroy $btn
    }
    set btn [itins::wset Button .btn -text "Top Cool Language" -command {try Freddie Mercury}]
    # comment this when you've got a toplevel hull, of course
    pack $btn
    update
    $btn configure -packpad 15
    tk_messageBox -message "The button text is : [$btn cget -text]"


July 17, 2005 SRIV When running the first example I get invalid command name ". The second example fails when not using the "save to file" widget creation mode. UPDATE: Heres some of the tweaks I had to make in order for it to work with no errors:

 itins.tcl:
 line 40:  eval itcl::class ::$className "\{\n$body\}"
 line 296: interp alias {} ::itins::delete {} ::itcl::delete object
 line 299: if {[llength $args] == 1} {set args [lindex $args 0]}

SRIV Wishlist of things I havent been able to figure out yet:

 try to eliminate the need for itins::wset
 add configure functionality where specifying no args returns all the current options, like real tk widgets

PWQ 18 Jul 05, I don't want to appear negative, but what is the point of an OO system on top of another OO system?

It's not. it uses pure tcl to modify the behavior of an OO system.

July 30, 2005 SRIV It appears that sarnold has implemented my wishlist, Thanks! Go grab the latest version from the url above.

Some notes on converting from Snit to itins:

  • Replace any occurrence of $self with $this
  • In the constructor, replace "$self configurelist $args" with "eval configure $args" - Sarnold thinks that "configure $args" should be enough
  • Replace references to option values such as $option(-text) with $text
  • You must have a destructor otherwise itins throws an error. At least use "destructor {}"
  • Do not create method names that are the same as a tcl command name in itins. Snit allows this though.

A pkgIndex.tcl file for itns:

 #pkgIndex.tcl for itins
 package ifneeded itins 0.2 [list source [file join $dir itins.tcl]]

RLH 05-Sept-2005: Why layer a delegation system on top of incr Tcl instead of just using Snit? SRIV 05-Sept-2005: Speed. RLH And does it? Do you have benchmarks? Just curious. SRIV The source includes a benchmark app. Give it a try. DKF Have you tried building on top of xotcl? That's reputed to be faster than itcl... SRIV The appeal of itcl/itins is that its easy to convert my widgets from snit, and I have itcl available anyways, since I use tclkit exclusively. So for me, its small, fast and sufficiently functional. If your operating circumstances are different, ymmv. Sarnold 06-Sept-2005: Yes, and my Mandrake Discovery provides itcl, but not xotcl. Here are some benchmarks using a 766 Mhz processor under WindowsME:

 Snit vs Itins : time spent in microseconds
 Test:                              Snit:   Itins:
 Creation and destruction:           3052      162
 Method call (not delegated):          84       14
 Method call (delegated):              76       43
 Option setting:                      113       34
 Option setting (delegated):          168       53
 Option getting:                       55       37
 Option getting (delegated):          110       57