optproc

slebetman: optproc proc allows you to define procs where the syntax for arguments behave like "options". Like what we often see in Tk. After the umpteenth time writing/using argument parsers to do this I thought there ought to be a better, more natural way:

  proc optproc {name args script} {
    proc $name args [
      string map [list ARGS $args SCRIPT $script] {
        foreach var {ARGS} {
          set [lindex $var 0] [lindex $var 1]
        }

        foreach {var val} $args {
          set [string trim $var -] $val
        }

        SCRIPT
      }
    ]
  }

The syntax is similar to proc except that when calling procs defined by optproc the arguments need to be accompanied by their name. Also when calling the created procedure the order of arguments doesn't matter. Default values for arguments are defined just like default values in proc. If no default is defined then the argument's value defaults to an empty string.

Example usage:

  optproc testproc {text {case "upper"}} {
    switch -- $case {
      upper {set text [string toupper $text]}
      lower {set text [string tolower $text]}
    }
    puts $text
  }

  # Calling "testproc":

  testproc -text Hello              ;# this prints out HELLO
  testproc -text Hello -case lower  ;# this prints out hello
  testproc -text Hello -case none   ;# this prints out Hello

A version that overrides proc is also available, scroll to the bottom of the page (or read on to find out why you may want that version instead of the simpler version above).

Lars H: An alternative approach, if you're willing to write e.g. $O(-case) instead of just $case, is to put all the optional arguments in an array:

 proc aoptproc {name arglist body} {
    set argdict {}
    foreach item $arglist {
       lappend argdict [lindex $item 0] [lindex $item 1]
    }
    uplevel 1 [list ::proc $name args "
       array set O [list $argdict]
       array set O \$args
       $body
    "]
 }

(The uplevel makes a difference if you call this from within a namespace.) In this case, the above testproc would come out as:

 aoptproc testproc {-text {-case "upper"}} {
    switch -- $O(-case) {
      upper {set res [string toupper $O(-text)]}
      lower {set res [string tolower $O(-text)]}
      default {set res $O(-text)}
    }
    puts $res
 }

Of course, from that it is only a tiny step to doing it all with just proc, like so:

 proc testproc {args} {
    array set O {-text {} -case "upper"}
    array set O $args
    switch -- $O(-case) {
      upper {set res [string toupper $O(-text)]}
      lower {set res [string tolower $O(-text)]}
      default {set res $O(-text)}
    }
    puts $res
 }

I tend to do the latter, but YMMV.


RS: Nice - simple and powerful! Note however that such optprocs will not be listed in tclIndex, as auto_mkindex goes only for calls to proc.

slebetman Hmm. The man page for auto_mkindex states that it parses files by sourcing them into a slave interp. Which means that optprocs should be listed since it is merely a wrapper around proc (as info procs would show).

RS "Calculemus!" as Leibniz said - Tcl 8.4.5, Win 95:

 /_Ricci/tmp> cat t.tcl
 proc optproc {name argl body} {proc $name $argl $body}
 interp alias {} _proc {} proc
 optproc f x {set x}
 _proc g x {set x}

 /_Ricci/tmp> echo auto_mkindex . | tclsh
 /_Ricci/tmp> cat tclIndex
 # Tcl autoload index file, version 2.0
 # This file is generated by the "auto_mkindex" command
 # and sourced to set up indexing information for one or
 # more commands.  Typically each line is a command that
 # sets an element in the auto_index array, where the
 # element name is the name of a command and the value is
 # a script that loads the command.

 set auto_index(optproc) [list source [file join $dir t.tcl]]
 /_Ricci/tmp>

So neither optproc nor the interp alias _proc which I usually use to hide procs from indexing are executed, and hence rerouted to proc; and so neither f nor g end up in the tclIndex. Looks like the interpreter skips commands not in core Tcl...

Lars H: Yes, that "sourcing" is more a way of parsing the script as a sequence of commands than normal evaluation. While the mechanism strikes me as obscure, I don't see a reason it shouldn't be possible to extend to understand new command-creating commands, but there's probably nothing that can be done from within the file being indexed. Ideally, the auto_index mechanism should provide a command that files could use to tell it about new command-creating commands they're going to use, e.g. after

  proc optproc {name args script} { ... }

one could go

  auto_mkindex_register optproc 0

to say "Hey, auto_mkindex, from now on optproc is a command defining new commands that I want indexed; you'll find the name of the command it defines in argument 0."

slebetman Hmm.. I knew I should have tested it out first. In any case, here's a version which is auto_mkindex friendly. Though I'm leaving the original implementation above as is since this implementation is much more messy:

  rename proc _proc
  _proc proc {name args} {
    if {[llength $args] == 3 && [lindex $args 0] == "-options"} {
      _proc $name args [
        string map [list \
          ARGS [lindex $args 1] \
          SCRIPT [lindex $args 2]
        ] {
          foreach var {ARGS} {
            set [lindex $var 0] [lindex $var 1]
          }

          foreach {var val} $args {
            set [string trim $var -] $val
          }

          SCRIPT
        }
      ]
    } elseif {[llength $args] == 2} {
      _proc $name [lindex $args 0] [lindex $args 1]
    } else {
      error {wrong # args: should be "proc name ?-options? args body"}
    }
  }

Example usage:

  proc testproc -options {text {case "upper"}} {
    switch -- $case {
      upper {set text [string toupper $text]}
      lower {set text [string tolower $text]}
    }
    puts $text
  }

NEM auto_mkindex does allow you to register new proc-like commands. The only documentation for this feature merely states that it is undocumented, and points you at auto.tcl. Just copying the proc version from there:

auto_mkindex_parser::command optproc {name args} {
    variable index
    variable scriptFile

    append index [list set auto_index([fullname $name])] \
	[format { [list source [file join $dir %s]]} \
	[file split $scriptFile]] "\n"
}