Two things that really annoy me with some programming languages: the lack of keyword (optional, always named and order agnostic) parameters and when the language has a builtin feature not made available to the user.
Tcl hits the nail here, because a lot of its standard commands have UNIX-like options but
it doesn't make any of this available in either proc
or apply
.
But since proc
can be wrapped "easily" (more later), a few hours gave me
this:
source .../util.tcl namespace path {::util ::tcl::mathop ::tcl::mathfunc} proc* p {-flag {-opt foo} x args} { puts [lmap v {flag opt x args} {string cat "$v=[set $v]"}] } p 1 a; p -opt bar -flag 1 a; p -flag; proc p1 {x} {} proc* p2 {-flag {-opt foo} x} {} timerate {p1 1}; timerate {p2 1};
Pretty nice? As you can see in the third example, I even implemented that Tcl behaviour where
mandatory positional parameter count is used to disambiguate them from options (which is how you
can do puts $string
without having to safeguard it with a --
marker).
Addendum about the aforementioned "easily" to show how the sausage is made; definitely not for the faint of heart.
proc proc* {name args body} { if {[llength $args] == 0 || ![string match "-*" [lindex $args 0]]} { proc $name $args $body return } while {[llength $args] && [string match "-*" [lindex $args 0]]} { set args [lassign $args[set args {}] arg] switch [llength $arg] { 1 { lappend optargs [set var [string range $arg 1 end]] lappend optinit 0 lappend optswitch $arg [list set $var 1] } 2 { lassign $arg opt init lappend optargs [set var [string range $opt 1 end]] lappend optinit $init lappend optswitch $opt "set [list $var] \[lindex \$args \[incr i]]" } default {error "$arg: unknown option format"} } } lappend optswitch default {error "$arg: unknown option"} set mcount 0 foreach arg $args { if {[llength $arg] != 1 || $arg eq "args"} break incr mcount } tailcall proc $name args [quasiquote { lassign `$optinit `@$optargs for {set i 0; set end [expr {[llength $args] - `$mcount}]} {$i < $end} {incr i} { set arg [lindex $args $i] if {![string match "-*" $arg] || $arg eq "--"} break switch -- $arg `$optswitch } apply `[list [list {*}$optargs {*}$args] $body]` \ `@[join [lmap o $optargs {string cat \$$o}]]` {*}[lrange $args $i end] }] }
Not too bad, you might say. And I agree, some very basic preprocessing and code generation at
play. No, the problem resides in that innocuous quasiquote
command used to do the
final generation.
You see, Tcl has a pretty poor metaprogramming story as it lacks a way to selectively subst
within strings, so what everybody does is… roll their own string based templating system! In
fact, I don't think a hacker can wield Tcl for some time without making something like it (cf the wiki).
In my case, here's the revolting but working result (ignore the ?
and move
helpers):
proc quasiquote {script} { set tmp [string map {\\ \\\\ $ \\$ \[ \\\[} $script] set tmp [string map {`@\\$ $} [move tmp]] set tmp [regsub -all {`\\\$([[:alnum:]_:]+|\{[^\}]+\})} [move tmp] {[list $\1]}] for {set start 0} {[regexp -indices -start $start {`@?\\\[} $tmp ropener]} {} { lassign $ropener rstart ropener_end set rend [+ [string first {]`} $tmp [+ $ropener_end 1]] 1] set splice [== [- $ropener_end $rstart] 3] set repl [string map {\\\\ \\ \\$ $ \\\[ \[} \ [string range $tmp $ropener_end [- $rend 1]]] set tmp [string replace [move tmp] $rstart $rend [? {$splice} {$repl} {\[list $repl\]}]] set start [+ $rstart [string length $repl] 1] } tailcall subst $tmp }
So horrible it breaks Emacs' (arguably fragile) tcl-mode indentation. I think there's only one valid reaction to the words "metaprogramming via regexp", especially coming from CL which inspired this:
