nxproc

Overview

nxproc is a Tcl extention enhancing user-defined procedures with named-argument syntax similar to many Tcl built-in commands. The extension provides 6 proc-like commands: nxproc, nxconstructor, nxmethod and -ci variations.

nxproc procedures are very close to "drop-in compatible" replacements for Tcl proc, method and constructor commands. This document describes syntax and features of the new commands as well as differences from Tcl built-in commands.

Installation

Download

Main repository: https://thinairarts.com/fossil/nxproc
Download: https://thinairarts.com/fossil/nxproc/download

Available in formats: tgz, zip

Install to appropriate directory

Once downloaded, decompress the archive in any convenient directory. Copy the nxproc1.0 directory to a location on the Tcl package path. (Usually */lib in the Tcl installation location.) Binaries are included for unix and Windows under tcl8 and tcl9 directories. (The supplied binaries were compiled for Tcl8.7 and 9.0. Using with Tcl8.6.11+ is supported. Recompiling for older Tcl versions will be necessary.)

Compiling from source

Full source code is included (in the ./generic directory). Compiling is straightforward. Configuration uses the configdef system. (Consult the configdef project documentation for more information.) Binaries (config and genPkgIndex are available in the ./bin directory.) Copying the runnable programs to location like /usr/local/bin assures availability.

Configuring for a given system requires modifying config.def in the target directory (i.e., unix/tcl8, etc.) with the path to installed Tcl location, desired nxproc installation directories, and other details. Alternatively, configuration parameters can be specified on the config command line. This is particularly useful as a one-time option, e.g., generating a profiling-enabled binary.

After modifying config.def it's only necessary to run config in the directory to generate the Makefile. Run "make && make install". nxproc should then be installed in the preselected Tcl installations and can be used with those versions of Tcl.

nxproc usage

Since the new commands introduced by this project have for the most part identical syntax and features, they'll be referred to collectively as "nxproc" throughout this document. Relevant differences among them will be made clear.

General syntax

Include package require nxproc in files where nxproc commands are used.

Syntax largely follows the conventions of Tcl proc command:

nxproc <proc name> {?fixed args? ?named args? ?**?} {
    <body>
}

Differences in arguments are described below. Proc name and body follow standard proc rules.

Object-oriented procedures

The package provides nxconstructor and nxmethod. Differences in syntax vs. nxproc are minimal, but share semantics. In total, the package adds 6 new procedures, nxproc, nxconstructor, nxmethod and their -ci variants.

nxproc arguments

nxproc procedures allow 4 kinds of input arguments: fixed, named-args, nxargs and nxunknown.

Fixed (positional) arguments are just like "regular" proc arguments, including the recent addition of optional defaults. Without defaults, fixed parameters are mandatory when the command is called:

% nxproc myproc {a b} {return "$a, $b"}

% myproc cat dog 
cat, dog
% myproc cat 
"Error: Missing value: arg 2 ('b')." 
% myproc cat dog antelope 
"Error: Expected '-' first char: antelope"

There are two styles of named arguments:

Note that type designators are not case sensitive, so "e", "E", "enum" and "ENUM" are all equivalent.

For string and numeric arguments, when a type designator isn't given, type is inferred as str or num. A type designator must be supplied for proper operation on bool and enum arguments. For their intended purpose, boolean values are restricted to the set of (0,1). However Tcl doesn't distinguish between boolean and general numeric values. A type designator is necessary to disambiguate the intended boolean from numeric values.

Enumerated values are lists. In Tcl at the script level lists and strings are not distinct. The type designator assures enumerated values are handled propely.

Consider these examples:

# implicit string
% nxproc p0 {-var0 cat} {
    return "v0==$var0"
}
% p0 -var0 dog
v0==dog

# implicit number
% nxproc p1 {-var1 32} {
    return "v1==[+ $var1 10]"
} 
% p1 -var1 120
v1==130

# explicit boolean
% nxproc p2 {-var2 b 0} {
    return "v2==$var2
}
% p2 -var2
v2==1

# explicit enum
% nxproc p3 {-var3 enum {cat dog bear cow}} {
    return "v3==$var3"
}
% p3
v3==cat
% p3 -var3 bear
v3==bear
% p3 -var3 giraffe
Error: Value 'giraffe' is not a member of enum 'var3'. 

# no type designator (e, enum)
% nxproc p4 {-var4 {cat dog bear cow}} {
    return "v4==$var4"
}
% p4
v4==cat dog bear cow ;# that is, p4 is a string...

nxproc also checks on argument type when nxproc is invoked and issues an error if (bool, numeric) types and values don't match:

% nxproc p5 {-var5 NUM 54x} {
    return "v5==$var5"
}
Error: <nxproc p5> '-var5': '54x' is not numeric.

Also, if items are missing nxproc signals an error:

% nxproc p5 {-var5 54 var6 "OK" } {
    return "v5==$var5, v6==$var6"
}
Error: <nxproc p5> 'var6'=malformed option. (Should be -var6)

% nxproc p6 {-var6 "Hello!" -var7 b } {
    return "v6==$var6, v7==$var7
}
Error: <nxproc p6> missing value (pos 4, after '-var7')

The final kind of argument is similar to "args" in proc. (Note that this kind of argument is valid when calling a created command, not when nxproc is creating commands.)

The nxargs feature collects all input after "-->, "--" or "->" appear on the command line.

# use nxproc to create myproc...call myproc...
myproc -abc abc -cde cde --> fgh ijk
# Everything after --> is collected in nxargs as a list...
puts $nxargs
# -> fgh ijk

Another example:

# create command (p7)
% nxproc p7 {a b -var8 "a string value"} {
    return "$a, $b, $var8, nxargs {$nxargs}"
  }

# call p7, -->, -- or -> marks end of arguments
% p7 one two -var8 "more stuff" --> filename.tcl something.else
one, two, more stuff, nxargs {filename.tcl something.else}

Finally, nxproc procedures can also handle unexpected arguments using the "two-star" or ** option. Including ** in the list of formals signifies that all unknown arguments and values will be available as a list in the procedure body variable nxunkown. When ** is included, no error is signaled due to unknown options/values entered on the command line.

% nxproc myproc {-abc a -cde b **} { 
    puts "nxunknown == $nxunknown" 
  }

# then ...
% myproc -abc abc MM OO PP -cde cde -XYZ XY" --> qrs tuv
# note that "MM OO PP" and "-XYZ XYZ" aren't known to myproc
nxunknown == MM OO PP -XYZ XYZ 

The nxunknown mechanism applies to the command line after fixed args. Since fixed arg input is terminated by the first "-xxx" appearing on the command line, it represents the first possible unknown argument.

% nxproc myproc {{a 192} {b wolf} **} {
    puts "$a, $b, && $nxunkown"
}

% myproc 444 -nothing important
444, wolf, && -nothing important

While nxproc does not have the standard "args" variable, it does provide these two args-like variables. In some ways they even extend the args idea, especially nxunknown which collects all unknown args regardless of placement. The nxunknown facility can be quite useful, for example constructing oo::class hierarchies.

nxproc checks numeric, boolean and enum values at creation and runtime:

% nxproc numproc {-numitem 88} {return $numitem}
% numproc -numitem 8b
Error: Option -numitem: value is not a number.

% nxproc boolproc {-boolitm b 0} {return $boolitm}
% boolproc -boolitm 10
Error: Option -boolitm: '10' not boolean value

% nxproc enumproc {-enumitm e {abc cde fgh}} {return $enumitm} 
% enumproc -enumitm ijk
Error: Value 'ijk' is not a member of enum 'enumitm'.

Default variables and values are printed to the terminal when "-?" or "-opt" is the argument to an nxproc procedure:

% source procx-test.tcl
% all -?
  _____ bool ____
     flagd: 0
     flagx: 0
     flagc: 1
     flagy: 1
  _____ str _____
   kitchen: modern stuff
    floors: linoleum
     walls: off-white
      auto: Honda
  _____ num _____
      frac: 0.5
      area: 2000
     rooms: 12
     doors: 4
  _____ enum ____
   critter: cat dog racoon deer ants birds
      food: vegetable fruit meat legume grain   

The nxproc-ci command

The nxproc-ci command is identical to nxproc except allowing named arguments to be called as upper or lower case (or any combination thereof). This can be convenient under some conditions.

nxproc-ci p1 {-person_name "name" -address "address"} {
    ...
    puts "$person_name"
    puts "$address"
    ...
}
p1 -Person_Name "Tom Thumb" -ADDRESS "111 Main Street"
Tom Thumb
111 Main Street

Note that in the procedure body variable names match exactly what was given to nxprox-ci (minus the leading dash). In the example, the body's variables are "person_name" and "address". The variable names are not altered or affected by the case of the command line arguments.

nxconstructor and nxmethod

These procedures replace constructor and method in TclOO code. Except for nx- specific features, syntax is essentially the same as the native procedures.

oo::class create MyClass {
    ... 
    nxconstructor {-name ...} { ... }
    nxmethod methodname {-arg0 val0 ...} { ... }
}

Previously described nx... patterns apply to nxconstructor/nxmethod. All features including -->/--/-> and ** are available and arguably even more useful in the TclOO environment. The full range of TclOO facilities are available to nx... procedures.

The dtype command

nxproc provides the dtype command which returns the nxproc type assigned to a variable when the procedure was created.

# dtype ?-n? <varname>
#   returns string value of type: bool, num, str or enum
#   with -n, returns numeric value: 101-104 

Profiling

nxproc can be compiled to activate profiling. This is accomplished by adding -DPROFILE to "DEFS" in the Makefile. This is handled in config.def by defining a Default value for "profile" as "1". Setting to "0" disables profiling.

(When profiling is turned off, profiling code has absolutely no effect on program behavior or performance. It's only compiled into the extension when intentionally activated.)

Profiling works by timing certain parts of the running program. Namely, it measures time required for handling fixed parameters, time needed for default variables, and time used to process command line options. Not only is the absolute time of each function important but also the ratio among them.

Running profile-enabled nxproc is most easily accomplished using the "prof" command from "nxtest.tcl" (in the "test" directory). It can be accessed through "procx-test.tcl", which has numerous test procedures already set up and ready to use. "prof" itself is created by nxproc.

The procdata command is a companion to nxproc. When a Tcl interpreter has used "package require nxproc", procdata is also available:

$ cd /path/to/procx/test
$ /usr/local/opt/tcl87b1s
% source procx-test.tcl
% procdata
currProcRec 27, lenprocx 30
prof       prof__2061f0__        482  -
test       test__fe9b0__        1085  -
ts         ts__4fa0d8__          136  -
tsA        tsA__b3c6c__          136  -
filter     filter__7d01a9__      329  -
filterA    filterA__8b571c__     329  CI
...

Without arguments procdata prints info about procedures created using nxproc. First column is the procedure name, followed by internal name, length (bytes) of procedure body. CI indicates procedure arguments are case-insensitive. When profiling is active procdata takes additional arguments that affect profile results. These arguments are handled by "prof".

# prof syntax:
# prof <arguments>
#   <name of procedure to profile>  (string, required)
#   -samples    (integer, default: 500000, optional)
#   -record     (boolean, default: 0, optional) (save all data points)
#   -cmdln      (boolean, default: 1, optional) (use command line)
#   -profiling  (boolean, default: 1, optional) (terminal print results)
# (prof results are written to file)
% prof str
prof str
input: str -auto {Ford F150} -kitchen spiffy!
----------------
>> 2.15 usec/sample  str  (samples 500000)
===============================================
Profiling results:
    FS filename: profile.prof__90e2a0__.csv
    Program: str -auto {Ford F150} -kitchen spiffy!
    Samples: 500000
    Medians: 20,269,330
    Ratios: sv/fix: 13.45  pa/sv:  1.23
===============================================

In this example, median time for fixed args was 20nsec, writing default variables was 269nsec and command line arguments took 330nsec. Ratio of last two results was 1.23.

% prof num
prof num
input: num -doors 8 -frac 0.874
----------------
>> 2.40 usec/sample  num  (samples 500000)
===============================================
Profiling results:
    FS filename: profile.prof__90e2a0__.csv
    Program: num -doors 8 -frac 0.874
    Samples: 500000
    Medians: 19,247,454
    Ratios: sv/fix: 13.00  pa/sv:  1.84
===============================================

A procedure with numeric inputs shows very similar timing for fixed args and default vars. However, time to parse command line args was greater vs. string input. Reasons for this difference are discussed in the "Performance" section.

In "config.def" in unix and win (tcl8 and tcl9) directories, the Defaults list includes "profile 0". Therefore running config -profile=1 generates a Makefile with "-DPROFILE". Then make clean; make install produces profile-enabled extension. Obviously, running config and remaining steps deletes profiling.

Testing and Performance

Included in the "test" directory are tests that can be used to gain information about nxproc procedures. Profiling with "prof" was discussed above. The "oonxtest.tcl" file contains a "runtests" method that provides information about performance.

# "runtest" syntax:
# Timer create tmr0
# tmr0 runtests  <type, function/effect> {default value}
#   -ls  <list of tests to run>                 {use tmrtm}
#   -start <int >= 0, begin at index in ls>     {0}
#   -stop <int <= length of ls, stop at ndx>    {ls length}
#   -filenm <where to write results>            {none/console output}
#   -fmode <a == append, w == truncate+write>   {a, append}
#   -meminfo <boolean, exec 'memory info'>      {off}
#   -noshow <boolean, true->no console output>  {off}
#   -nocfg <boolean, true->don't use list config {off}

In general performance characteristics of nxproc procedures are good to excellent. Performance is affected by several factors.

First, nxproc procedures are not as performant as native Tcl procs. The basic reason for this is, as noted above, nxproc produces an "inner" command enclosed in a Tcl proc. In effect it's a proc call within a proc call. While unavoidably common in Tcl code it nonetheless has some, if usually modest, overhead.

In general, fixed args (with or without defaults) are faster than named args especially when there are several. As a rule, the greater the number of named args the longer it will take to accommodate all inputs. Likewise, longer command lines contribute to longer command runtime. These effects vary by type of input, length of lists to look through and related factors.

OTOH nxargs processing confers almost no performance penalty, whereas nxunknown can be slower. This is due to the fairly involved task of sorting unknown elements on the command line from expected input.

In any case, in the tmrtm suite most tests completed in under 2 microsec on a Windows laptop (11th gen processor, 16GB ram) with results varying on running under Windows 11 directly or in a WSL2 Linux terminal. No test required more than 2.5 microsec. Results also depend on versions of Tcl employed. There is considerable info available using a symbol-enabled Tcl. This is of course roughly 10 times slower compared to a "normally" compiled Tcl. In brief, nxproc was only 5-7% slower and without greater memory usage vs. standard versions.

More complex tests have to be judged by different standards. The test suite can evaluate creating a test website. This of course is orders of magnitude slower. A typical performance was ~400 microseconds/iter on regular Tcl and ~4000 microsec/iter on symbols-mem compiled Tcl. Performance in such applications is normally not crucial and is in practice very satisfactory for such purposes.

Where performance is important it's recommended to use standard proc/method calls. nxproc may suffice if fixed arg/nxargs use will work. Named arguments are an excellent approach to configuration/initialization where many input parameters would be expected. By its nature this use case is seldom time-critical, so application performance will not be affect adversely.