Updated 2018-09-17 23:55:38 by kpv

Keith Vetter 2018-05-03: @decorators.tsh -- A tcl implementation of Python decorators

In Python, decorators are syntactic sugar that lets you wrap a function to provide some extra functionality [1]. They're used for a bunch of different reasons, from memoization and timing to static methods and getters/setters[2].

Once you start thinking in terms of wrapping functions, it's easy to come up with more and more instances when they can be very helpful.

Here's a short list of some useful tcl decorators that I've come up with in the past few months:

  • @namedArgs -- lets you call functions like myFunc var1=value1 var2=value2
  • @tip288 -- implementation of tip288, args anywhere in the procedure argument list
  • @memoize -- automatically memoizes any function
  • @autoIndex -- allow a+b type arguments (ala lindex) for any function
  • @passByReference -- turns all &arg into a pass by reference argument
  • @debug -- prints the arguments a function is called with and its return value
  • @time -- prints how much time a function took to execute

The syntax mimics Python:
 @namedArgs \
 proc MyFunction {...} {...}

Here are the implementations and an example how to use each one:
proc @namedArgs {defaults p pname pargs lambda} {
    # Creates dictinary argsDict with values in $defaults merged
    # with all key=value items in $args
    if {$p ne "proc"} { error "bad syntax: $p != 'proc'" }
    if {[lindex $pargs end] ne "args"} {
        proc $pname $pargs $lambda
        return
    }
    set body "
        set argsDict \[dict create $defaults\]
        set newArgs {}
        foreach arg \$args {
            if {\[regexp {^(.*)=(.*)$} \$arg . key value\]} {
                dict set argsDict \$key \$value
            } else {
                lappend newArgs \$arg
            }
        }
        set args \$newArgs
        $lambda
    "
    proc $pname $pargs $body
    return $pname
}

@namedArgs {name1 default1 name2 default2 name3 default3 name4 default4} \
proc test_namedArgs {args} {
    puts "In test_namedArgs with argsDict: "
    set longest [tcl::mathfunc::max 0 {*}[lmap key [dict keys $argsDict] {string length $key}]]
    dict for {key value} $argsDict {
        puts [format "    %-${longest}s = %s" $key $value]
    }
}
test_namedArgs name1=value1 name3=value3 other args name4=value4

# ================

proc @tip288 {p {pname ""} {pargs ""} {lambda ""}} {
    if {$p ne "proc"} {
        if {$pname ne "" || $pargs ne "" || $lambda ne ""} {error "bad synax: $p != 'proc'"}
        set pname $p
        set pargs [info args $pname]
        set lambda [info body $pname]
    }
    set idx [lsearch $pargs "args"]
    if {$idx == -1 || $idx == [llength $pargs] - 1} {
        proc $pname $pargs $lambda
        return
    }
    set pre [lrange $pargs 0 $idx]
    set post [lrange $pargs $idx+1 end]
    set body "
        set args \[lreverse \[lassign \[lreverse \$args\] [lreverse $post]\]\]
        $lambda
    "
    proc $pname $pre $body
    return $pname
}

@tip288 \
proc test_@tip288 {a b args c d} {
    set msg "a: '$a' b: '$b' c: '$c' d: '$d' =>  args: '$args'"
    puts $msg
    return $msg
}
test_@tip288 A B these are random arguments for testing C D

# ================
proc @memoize {p pname pargs lambda} {
    if {$p ne "proc"} { error "bad synax: $p != 'proc'"}

    proc $pname $pargs "
        set cmd \[info level 0\]
        if {\[info exists ::MEM(\$cmd\)\]} { return \$::MEM(\$cmd) }
        set argVals \[lmap var {$pargs} {set \$var}]
        set ::MEM(\$cmd) \[apply {{$pargs} {$lambda}} {*}\$argVals\]
    "
}

@memoize \
proc test_@memoize {a b} {
    puts "in test_@memoize $a $b"
    return $a
}
test_@memoize 1 2
test_@memoize 1 2

# ================
proc @autoIndex {p pname pargs lambda} {
    if {$p ne "proc"} { error "bad synax: $p != 'proc'"}

    proc $pname $pargs "
         set argVals {}
         foreach arg {$pargs} {
             set val \[set \$arg\]
             if {\[regexp \{^-?\\d+\[+-\]-?\\d+$\} \$val\]} { set val \[expr \$val\] }
             lappend argVals \$val
         }
         apply {{$pargs} {$lambda}} {*}\$argVals
    "
}

@autoIndex \
proc test_autoIndex {a b c} {
    puts "a is $a and b is $b and c is $c"
}
test_autoIndex hello 3 4+5

# ================
proc @passByReference {p {pname ""} {pargs ""} {lambda ""}} {
    if {$p ne "proc"} {
        if {$pname ne "" || $pargs ne "" || $lambda ne ""} {error "bad synax: $p != 'proc'"}
        set pname $p
        set pargs [info args $pname]
        set lambda [info body $pname]
    }
    set prefix ""
    foreach arg [lsearch -all -inline -glob $pargs &*] {
        append prefix "upvar 1 \${$arg} [string range $arg 1 end];\n"
    }
    proc $pname $pargs "$prefix$lambda"
    return $pname
}

@passByReference \
proc test_@pbr {arg1 &who} {
    puts "in test_@pbr: arg1='$arg1' who='$who'"
    set who "new value for my global variable"
    return
}
set myGlobal "my global variable"
puts "myGlobal before call: $myGlobal"
test_@pbr xxx myGlobal
puts "myGlobal after call: $myGlobal"

# ================
proc @debug {p pname pargs lambda} {
    if {$p ne "proc"} { error "bad syntax: $p != 'proc'" }
    proc $pname $pargs "
        set msg \"DEBUG: calling $pname \"
        foreach arg {$pargs} {
            append msg \"\$arg=\[set \$arg\] \"
        }
        puts \$msg
        try {
            set start \[clock microseconds\]
            set argVals \[lmap var {$pargs} {set \$var}]
            set rval \[apply {{$pargs} {$lambda}} {*}\$argVals\]
        } finally {
            puts \"DEBUG: $pname returned \$rval\"
        }
     "
}

@debug \
proc test_debug {a b c} {
    puts "a: $a b: $b c: $c"
    return [string length $a]
}
test_debug 1 2 3

# ================
proc @time {p {pname ""} {pargs ""} {lambda ""}} {
    if {$p ne "proc"} {
        if {$pname ne "" || $pargs ne "" || $lambda ne ""} {error "bad synax: $p != 'proc'"}
        set pname $p
        set pargs [info args $pname]
        set lambda [info body $pname]
    }
    proc $pname $pargs "
        try {
            set start \[clock microseconds\]
            set argVals \[lmap var {$pargs} {set \$var}]
            return \[apply {{$pargs} {$lambda}} {*}\$argVals\]
        } finally {
            puts \"$pname took \[expr {\[clock microseconds\] - \$start}\] microseconds\"
        }
     "
    return $pname
}
@time \
proc test_timing {n} {
    puts "in test_@timing: $n"
    after $n
    return "n is $n"
}
test_timing 500