Description edit
Richard Suchenwirth 2001-04-06 21:37:59:I have toyed with the gadget concept every now and then for years now (see tally: a string counter gadget or A matrix gadget), but here's a generalized approach. See also LOST for Larry Smith's extension to that.. and On Things for a more radical typeless approach.I call gadgets "poor man's objects" - basically they are a variable (string, list or array) and a proc, both with the same name. The proc is renamed away when the variable dies. No inheritance (yet), no namespaces involved, but they offer a slick Tk-like API where you call the gadget's name with a minor command (method name) and possibly other parameters.First, a look at the generalized API. You can specify a gadget type with methods, in which you can refer to 'args' for the method's arguments (parse'em yourself ;-) and 'self' for the variable itself, like this:gadget type number { = {set self [expr $args]} ++ {set self [expr $self+1]} round {set self [expr round($self)]} sqrt {expr sqrt($self)} } gadget type int { = {set self [expr {round($args)}]} ++ {incr self} } gadget type Array { = - += {eval array set self $args } -= {catch {unset self($args)}} @ {set self($args)} empty {expr [array size self]==0} names {array names self} {} {array get self} } gadget type List { = {eval set self $args} += {lappend self $args} @ {lindex $self $args} empty {expr [llength $self]==0} sort {lsort $self} length {llength $self} } gadget type File { = {set self [eval open $args]} >> {upvar $args var; expr [gets $self var]+1} << {puts $self $args} eof {eof $self} open? {expr ![catch {seek $self 0 current}]} close {close $self} }For a defined gadget type, you can call a "constructor" with a name, and possibly an initialization:
number N = 1.5 Array A List L = {foo bar}For a "destructor", we just reuse the good old [unset] wheel.Now you can use these variables or procs as you wish, with the addition that calling a (non-array) gadget proc without arguments returns its value, so [N] is a new alternative to $N and [set N]:
N = [N] * $N ;# -> 2.25 (just for the fun of it ;) set A(cat) Katze ;# -> Katze A names ;# -> cat L += grill L = [L sort] puts "[L] has [L length] elements, second is [L @ 1]" #; -> bar foo grill has 3 elements, second is foo File F = gadget.tcl int i = 1 while {[F >> line]} { puts [i]:$line i ++ } F closeYes, this is still Tcl, and no, it's not like in the book. You can adjust the language pretty much to your likings via the method names. Arithmetic assignments look almost like all the world expects them to look (cf. Radical language modification, where I tried the same goal with the unknown command), and by pressing the assigned value through [expr] via number = or int = method, some typechecking is introduced.The polymorphism (same method names for different types) allows some hiding of internal quirks, e.g. now you can increment a double like an int with ++, by just adding 1 to it, resp. calling [incr]. Notice also the polymorphism of += for lists vs. arrays: append an element, or set a key-value pair.For introspection, you can get the types and names defined, and each gadget tells his type if asked:
gadget types ;# -> number int Array List File gadget names ;# -> N A L F N type ;# -> numberBook-keeping of names and types is done not with global variables, but with procs whose bodies are rewritten when needed. [proc gadget::names] shows how that's done: start with a argumentless list command, append a new name on gadget creation, lreplace the name out on gadget destruction.OK, so here's the code that does that (not very long, but not the easiest reading either - after all you write a proc that writes a proc that writes a proc ;-): The switch line containing 'type' did not work on my system: adding eval worked like a charm. Has there been a change in how the switch command works since 2002? -- Jim Hinds
History edit
PYK 2013-11-29: Added namespace, and test suite; made generated code more robust by encapsulating interpolated values in list format.Code edit
#! /bin/env tclsh namespace eval gadget { namespace export * namespace ensemble create proc gadget {cmd args} { switch -- $cmd { names names type {type {*}$args} types types default {return -code error "$cmd? should be name, type, or types"} } } proc types {} {} proc names {} {} proc type {type methods} { if {[namespace which [namespace current]::$type] ne {}} { return -code error "type $type redefines existing command" } set types [gadget types] if {$type ni $types} { proc [namespace current]::types {} [list return [list {*}$types $type]] } set template { proc @type@ {name args} { set ns [uplevel namespace current] if {$name eq {}} { set name [lindex $args 0] upvar 2 $name self set rest [lindex $args 1] set cmd [lindex $rest 0] set args [lrange $rest 1 end] switch -- $cmd { type {return @type@} @methods@ {} {set self} default {return -code error\ "$cmd? Should be one of: [list type {*}@cmds@]"} } } else { if {[namespace which ${ns}::$name] ne {}} { error "gadget $name redefines existing command" } trace variable ${ns}::$name u [list [namespace current] unset $ns] proc [namespace current]::names {} [list {*}[info body [namespace current]::names] ${ns}::$name] proc ${ns}::$name {args} "[list [namespace current]] @type@ {} [list $name] \$args" if {[llength $args]} {uplevel [list ${ns}::$name] $args} } } } set cmds {} foreach {cmd -} $methods {lappend cmds $cmd} set template [string map [list @cmds@ $cmds @type@ [ list $type] @methods@ $methods] $template[set template {}]] if 1 $template set type } proc unset {ns name el -} { if {$el eq {}} { rename ${ns}::$name {} set names [info body [namespace current]::names] set where [lsearch $names ${ns}::$name] proc [namespace current]::names {} [lreplace $names $where $where] } } proc test {} {namespace eval test { package require tcltest namespace import ::tcltest::* gadget type number { = {set self [expr $args]} ++ {set self [expr $self+1]} round {set self [expr round($self)]} sqrt {expr sqrt($self)} } gadget type int { = {set self [expr {round($args)}]} ++ {incr self} } gadget type Array { = - += {eval array set self $args } -= {catch {unset self($args)}} @ {set self($args)} empty {expr [array size self]==0} names {array names self} {} {array get self} } gadget type List { = {eval set self $args} += {lappend self $args} @ {lindex $self $args} empty {expr [llength $self]==0} sort {lsort $self} length {llength $self} } gadget type File { = {set self [eval open $args]} >> {upvar $args var; expr [gets $self var]+1} << {puts $self $args} eof {eof $self} open? {expr ![catch {seek $self 0 current}]} close {close $self} } test gadgets {} { gadget number N = 1.5 gadget Array A gadget List L = {foo bar} N = [N] * $N ;# -> 2.25 (just for the fun of it ;) lappend res [N] #test bad method catch {N flyawayhome} eres eopts lappend res $eres set A(cat) Katze ;# -> Katze lappend res $A(cat) lappend res [A names] ;# -> cat L += grill L = [L sort] lappend res "[L] has [L length] elements, second is [L @ 1]" #; -> bar foo grill has 3 elements, second is foo lappend res [gadget types] unset N catch {N} eres eopts lappend res "invalid command after unset: [string match {*invalid command name*} $eres]" return $res } [list 2.25 \ {flyawayhome? Should be one of: type = ++ round sqrt} \ Katze \ cat \ {bar foo grill has 3 elements, second is foo} \ {number int Array List File} \ {invalid command after unset: 1} ] cleanupTests }} } if { [info exists argv0] || [file tail [info script]] eq [file tail $argv0]} { gadget::test }Notes: Gadgets can be passed by name in proc calls, where you either reuse the same name, or register the upvar variable:
upvar $name $name ;# or: upvar $name var; [$name type] varIn order to avoid conflicts, gadget names should be unique and should not use existing command (C or Tcl) names. This prevents one from accidentally rewriting "set" or other Tcl essentials. Drawback: gadgets with same name cannot be used in recursive procs.
- Shorter and Sweeter:
proc gadget { unused type methods } { set typeproc { set typeproc { upvar @var self @type self $method $args } upvar $var self if { "$method" == "" } { return $self } switch $method { @methods } regsub @var $typeproc $var typeproc proc $var { { method "" } args } $typeproc } regsub @type $typeproc $type typeproc regsub @methods $typeproc $methods typeproc proc $type { var method args } $typeproc }RS: I find it more legible, though, if all four mentions of typeproc inside the first set typeproc .. are replaced by instproc, as that is dealing with the instance proc anyway.AMG: The [regsub]s can be replaced with [string map]s:
proc $var {{method {}} args} [string map [list @var $var] $typeproc] proc $type {var method args} [string map [list @type $type @methods $methods] $typeproc]Whoa, to avoid unwanted expansion, better put an extra level of listiness in there:
[string map [list @var [list $var]] $typeproc]You can even lose the $typeproc variables, instead directly including the template. To make things easier still, make a helper procedure to do the string mapping. Yet more: make that helper procedure itself make procedures!
proc proc_template {name lazy_args eager_args body} { set map [list] foreach varname $eager_args { upvar 1 $varname var lappend map @$varname@ [list $var] } proc $name $lazy_args [string map $map $body] } proc gadget {unused type methods} { proc_template $type {var method args} {type methods} { upvar $var self if {$method eq ""} { return $self } switch $method @methods@ proc_template $var {{method ""} args} {var} { upvar @var@ self @type@ self $method $args } } }There's a pitfall here, though:
gadget type @var@ {= {set self [expr $args]}} @var@ name = 5 name ERROR: too many nested evaluationsThe outer [proc_template] expands the inner @type@ to @var@, and the inner [proc_template] expands both @var@s to name, resulting in proc name {{method ""} args} {upvar name self; name self $method $args}. Bad!Does anyone have any suggestions for how to make this safe?
Method inheritance (even multiple) can be had cheaply if the methods to be inherited are spliced in after @methods above. The switch might sometimes run longer, but every method not defined for your type would fall through to the first inherited method of same name.