Named arguments

Richard Suchenwirth 2004-01-15 - Arguments to commands are mostly by position. But it's very easy to add the behavior known from Python or Ada (?), that arguments can be named in function calls, which documents the code a bit better, and allows any order of arguments.

The idea (as found in Welch's book) is to use an array (here called opt) keyed by argument names. Initially, you can set some default values, and possibly override them with the args of the proc (which has to be paired, i.e. contain an even number of elements):

 proc replace {s args} {
   array set opt [concat {-from 0 -to end -with ""} $args]
   string replace $s $opt(-from) $opt(-to) $opt(-with)
 }
#--- Testing:
 % replace abcdefg -from 3 -to 4 -with xx
 abcxxfg

Keywords can come in any order or be omitted (in which case the defaults apply). Flaw: "Undefined" keywords, e.g. typos (-wiht), are not detected...


Here's one which catches (but does not identify) typo's -jcw

 proc replace {s args} {
   array set opt {-from 0 -to end -with ""}
   set n [array size opt]
   array set opt $args
   if {$n != [array size opt]} { error "unknown option(s)" }
   string replace $s $opt(-from) $opt(-to) $opt(-with)
 }

RS feels prompted to reply with an error-identifying version, changed to using the "anonymous array" "" instead of opt, which makes access to such arguments look better:

 proc replace {s args} {
   array set "" {-from 0 -to end -with ""}
   foreach {key value} $args {
      if {![info exists ($key)]} {error "bad option '$key'"}
      set ($key) $value
   }
   string replace $s $(-from) $(-to) $(-with)
 }

As the boilerplate is getting a bit lengthy, yet looks like it might be used more than once, here it is factored out (and with improved error message):

 proc named {args defaults} {
    upvar 1 "" ""
    array set "" $defaults
    foreach {key value} $args {
      if {![info exists ($key)]} {
         error "bad option '$key', should be one of: [lsort [array names {}]]"
      }
      set ($key) $value
    }
 }

#--- The use case now looks pretty slick again:

 proc replace {s args} {
   named $args {-from 0 -to end -with ""}
   string replace $s $(-from) $(-to) $(-with)
 }
#--- Testing:
 % replace suchenwirth -from 4 -to 6 -with xx
 suchxxirth
 % replace suchenwirth -from 4 -to 6 -witha xx
 bad option '-witha', should be one of: -from -to -with

KPV Nice, but I like to abbreviate my options. So here's a slightly tweaked version that allows you to use shorter versions of options names (as long as they're unique). It also uses DGP's better error handling if possible.

 proc named {args defaults} {
    upvar 1 "" ""
    array set "" $defaults
    set aname [lsort [array names {}]]
    
    foreach {key value} $args {
        if {! [info exists ($key)]} {           ;# Check for unique prefix
            set possible {}
            foreach keyname $aname {
                if {[string first $key $keyname] == 0} {
                    lappend possible $keyname
                }
            }
            if {[llength $possible] != 1} {     ;# Not a unique prefix
                set emsg [expr {[llength $possible] ? "ambiguous" : "bad"}]
                append emsg " option \"$key\": must be [join $aname {, }]"
                regsub {(.*),} $emsg {\1 or} emsg
                if {$::tcl_version >= 8.5} {
                    return -code error -level 2 $emsg
                }
                error $emsg
            }
            set key $possible
        }
        set ($key) $value
    }
 }

DGP Can be a little slicker using a Tcl 8.5 feature. Where you have

         error "bad option '$key', should be one of: [lsort [array names {}]]"

in [named], replace it with

         return -code error -level 2 "bad option '$key', should be one of: [lsort [array names {}]]"

for a cleaner stack trace.


jcw - Neat. FWIW, my personal usage style pref would be:

 proc replace {s args} {
   defargs -from 0 -to end -with ""
   string replace $s $(-from) $(-to) $(-with)
 }

Note that it's quite nice now, but not the same as Python's idiom, which lets you mix required, optional, default, and named args. Required args etc can also be passed as named args. And named args can also be new ones, not listed in the definition.


procargs is a proc that I wrote some time ago that allows one off switchified procs, existing procs to be switchified and the overloading of proc itself. I believe that it addresses the above complaints about not being as general as python (but I know nothing about python). It defines the defaults in the usual proc way and extracts the switch names and values using tcl's info introspection JBR.

 procargs replace { str { from 0 } { to end } { with {} } {
     string replace $str $from $to $with
 }

I like this better since it overloads the existing proc API. It is easy to overload proc, source a package and call procs in the package with named args.

Can you give an example of use? Does procargs affect error tracebacks? -jcw

Hmmm... I thought that the above was an example of its use. The code there creates a proc named replace that can be called as the other replace procs on this page:

 puts [replace abcdefg -from 3 -to 4 -with xx]
 abcxxfg

There was a small typo in the error handling which I fixed, and an error in switch specification reports a coherent error message but adds a level to the error stack.

slebetman: See also optproc for another implementation of this idea. Although my implementation assumes all arguments are named its implementation is much simpler. Example:

  optproc replace {str {from 0} {to end} with} {
    string replace $str $from $to $with
  }
  
  replace -str abcdefg -from 3 -to 4 -with xx

Larry Smith Take a look at init. Small and simple, but powerful. Also, see this one: Matthias Hoffmann - Tcl-Code-Snippets - misc routines - command line parsing.


MNP - 6/11/05 - Rather than write my own, I just use tcllib's cmdline to process named arguments for me. For example,

  package require cmdline
  proc foobar {args} {
    set myoptions {
      {bar.arg 5}
    }
    array set arg [::cmdline::getoptions args $myoptions]
    ### your code using named arguments here ###
  } ;# end proc foobar

Example usage: foobar -bar 99

The getoptions command is particularly nice since it will fail if you pass it a bogus named parameter. Alternatively, you can use getknownopts to process the known/defined options and leave the variable with a list of just the remaining unknown elements.


NEM - There is also, of course, the opt package which still ships with the Tcl core. However, the file implementing it starts with a warning that the code is due to be removed from Tcl at some point. Anyone know why? Personally, my preference for named args would be that any parameter of a proc can be called using -name value notation, so e.g. if I do:

 proc foo {a b {c 12}} { ... }

I can call it in lots of different ways:

 foo 1 2 ;# a = 1, b = 2, c = 12
 foo -c 15 1 2; # a = 1, b = 2, c = 15
 foo 1 2 -c 15 ;# ditto
 foo -b 12 2 ;# a = 2, b = 12, c = 12

etc. See [1 ] for a discussion (a little further down). I have a version of lambda sitting around somewhere that does this, and has lexical closures -- makes writing OO/widget frameworks fairly simple. Interfacing with the option db would be a bit more work.


RS 2005-09-21: Another simplistic variation, which uses one LOC to override defaults by name:

 proc demo {foo args} {
   #--------- first, set defaults
   set bar 1; set grill 0
   #--------- possibly override local variables from args:
   foreach {name val} $args {set $name $val}

   #--------- now for the action...
   return [list foo $foo bar $bar grill $grill]
 }
 % demo hello
 foo hello bar 1 grill 0
 % demo hello bar 2 grill 3
 foo hello bar 2 grill 3
 % demo hello grill 3 
 foo hello bar 1 grill 3

PWQ 28 Sept 05 , From discussion on Argument Parsing, a discussion, I present here a simple and efficient approach for those people that want named vs positional arguments, ie we support both.

 # Process the named arguments or positional parameters.
 proc userargs {arglist defaults theargs} {
    set ai -1
    uplevel 1 [list foreach $arglist  $defaults {break}]
    set args $theargs ;# replace this with arg/default pairing ...
    for {set i 0} {$i < [llength $args]} {incr i} {
       switch -- [set v [lindex $args $i]] {
          {} -
          - {
             incr ai
          }
          default {
             if {[lsearch -exact $arglist $v] != -1 } {
                uplevel 1 [list set $v [lindex $args [incr i]]]
             } else {
                uplevel 1 [list \
                        set [lindex $arglist [set ai [expr {($ai+1) % [llength $arglist]}]]] $v ]
             }
          }
       }
    }
 }

 # 
 proc myproc {cmd args defaults body} {proc $cmd {args} "[list userargs $args $defaults] \$args\n$body" }

An Example:

 myproc fred {a b c} {- - 3} { puts "My args are $a $b $c" }
 fred 123 c 509 b "this is a demo"

A astute programmer will of course note that they would infact rename 'proc' rather than creating yet another function (unless they are OO programmers and then they will just spend all day refactoring their classes).

Also I should have presented the example that uses the default syntax of:

    {arg {def}} {arg {def}} ....

Rather than suppliing two arguments to the myproc command, but I was too lazy to do that, and besides, why should I do everyones homework assignments!

(DKF corrected myproc implementation to do what PWQ intended.). PWQ Taken to conclusion Named Arguments - By Specialisation


MG offers another alternative on Sep 29th 2005, probably more useful for [mega]widgets than procs, at Named Arguments (MG)


Roy Keene offers another implementation that uses the exact same syntax as the [proc] command

 namespace eval ::naproc {
         proc ::naproc::naproc {procname procargs procbody} {
                 set procbody "\n\t::naproc::handleargs [list $procname] [list $procargs] \$args;$procbody"
                 uplevel [list proc $procname args $procbody]
         }
 
         proc ::naproc::handleargs {procname procargs calledargs} {
                 if {[string index [lindex $calledargs 0] 0] != "-"} {
                         # Emulate unnamed calling convention
                         for {set i 0} {$i < [llength $procargs]} {incr i} {
                                 unset -nocomplain val
                                 set arg [lindex $procargs $i]
                                 if {[llength $arg] == 2} {
                                         set val [lindex $arg 1]
                                 }
                                 set arg [lindex $arg 0]
                                 if {$i < [llength $calledargs]} {
                                         set val [lindex $calledargs $i]
                                 }
                                 if {![info exists val]} {
                                         uplevel [list error "wrong # args: should be \"$procname $procargs\""]
                                 }
                                 uplevel [list set $arg $val]
                         }
 
                         return
                 }
                 foreach {arg val} $calledargs {
                         if {[string index $arg 0] != "-"} {
                                 continue
                         }
 
                         set arg [string range $arg 1 end]
                         set arginfo($arg) $val
                 }
 
                 foreach arg $procargs {
                         unset -nocomplain val
                         if {[llength $arg] == 2} {
                                 set val [lindex $arg 1]
                         }
                         set arg [lindex $arg 0]
 
                         if {[info exists arginfo($arg)]} {
                                 set val $arginfo($arg)
                         }
 
                         if {![info exists val]} {
                                 uplevel [list error "wrong # args: should be \"$procname $procargs\""]
                         }
                         uplevel [list set $arg $val]
                 }
         }
 }
 
 ::naproc::naproc test1 {joe bob {sally 3}} {
         puts "joe = $joe, bob = $bob, sally = $sally"
 }
 
 proc test2 {joe bob {sally 3}} {
         puts "joe = $joe, bob = $bob, sally = $sally"
 }

 puts "Test 1" 
 test2 1 2
 test1 -bob 2 -joe 1
 test1 1 2
 
 puts "Test 2" 
 test2 8 2 16
 test1 -bob 2 -joe 8 -sally 16
 test1 8 2 16
 
 puts "Test 3" 
 test1 -bob 3

AMG: How about [dict with args]?

proc myproc {args} {
   dict with args {}
   puts "$left=$right"
}
myproc left 2+2 right 4