Headline:Argument Parsing for Expert Systems
Date:Sunday, May 12, 2019
Posted By:Plaid Hatter Games

I have been finding myself falling into a pattern with procedural ship generating scripts that feel out of my earlier work on Tcl Improvement Proposal 479. That TIP was originally supposed to provide a formalized way to provide named arguments in Tcl. The tip hasn't gone anywhere for several reasons. The principle reason is that there is a lucrative bounty dangling from Flight Aware to make named arguments work inside the Tcl core. At the same time, there is a competing concept to named arguments called parse_args. But neither Tip479 nor Parse_args does all of what FlightAware stipulates in the bounty. (There is a third plan, tip457 which completely rewrites the contract of what developers should expect from the proc command that I list out of completeness, and can only hope never sees the light of day.)

Fortunately I wrote the implementation of that TIP as something that could be adapted to running in pure script or a C accelerated Tcl extension. One of the benefits of rolling my own Tcl distribution is that I can add extra packages to the core I distribute.

When I designed the tip, I was just looking to handle cases where a procedure or method required a LOT of arguments. And after the first 10 or so, it gets really hard to keep track of them all. And worse, adding or removing an argument requires a lot of hunting down and modifying all of the calls inside of one's code.

With dictargs, all arguments are listed as a key/value pair. Or at least all arguments beyond the arguments with a fixed position.

Here is an example of a procedure with traditional positional arguments:

proc sphere {radius color shell_thickness {material steel}} {
  set id [next_object_id]
  set total_volume [expr {$radius*$radius*$radius*4.0/3.0*pi()}]
  set hollow_volume [expr {$shell_thickness*$shell_thickness*$shell_thickness*4.0/3.0*pi()}]
  dict set ::all_objects $id class  sphere
  # Calculate some properties
  set density [material_density $material]
  dict set ::all_objects $id volume $total_volume
  dict set ::all_objects $id mass   [expr {($total_volume-$hollow_volume)*$density}]
  dict set ::all_objects $id density $density
  # Store whatever the user gave us
  dict set ::all_objects $id radius $radius
  dict set ::all_objects $id color $color
  dict set ::all_objects $id shell_thickness $shell_thickness
  dict set ::all_objects $id material $material
}
sphere 100 green 0

Here is that same procedure with dictargs:

dictargs::proc sphere {
  radius { description: {Radius of the sphere}}
  color  { description: {Specular Color of the outside}}
  shell_thickness {description: {Thickness of the outer shell 0=solid} default: 0}
  material {description: {Material of shell} default: steel}
} {
  set total_volume [expr {$radius*$radius*$radius*4.0/3.0*pi()}]
  set hollow_volume [expr {$shell_thickness*$shell_thickness*$shell_thickness*4.0/3.0*pi()}]
  set id [next_object_id]
  dict set ::all_objects $id class  sphere
  # Calculate some properties
  set density [material_density $material]
  dict set ::all_objects $id volume $total_volume
  dict set ::all_objects $id mass   [expr {($total_volume-$hollow_volume)*$density}]
  dict set ::all_objects $id density $density
  # Store whatever the user gave us. However, we don't need to spell out what
  # we received, we can just disgorge the contents of the args variable
  dict for {f v} $args {
    dict set ::all_objects $id $f $v
  }
}

sphere radius 100 color green shell_thickness 0

The dictargs style is far wordier for both the procedure definition and the actual command usage. But there is less "magic" about what is begin fed in and what the procedure is expected to do with it.

Honestly, it is down to style more than function. Though I will say, if one's style is too terse, the function tends to degrade to useless if the software ever needs maintenance programming years later.

The case I hadn't considered when designing tip479 is, what if you are starting off with a general function, and while you want to feed in data, that arguments may not mean anything to the top level command, but could man the world to something to one of the subordinate commands it calls.

dictargs::proc shape {
  class {description: {Primitive Class}}
} {
  set id [$class {*}$args]
  # Do something with that new item
  render_shape $id
  return [dict get $::all_objects $id mass]
}

shape class sphere radius 100 color green
shape class cube   length 100 width 100 height 100 color green

Assume I had a cube command that did all of the same things that the sphere command did, just for cubes. We can see that writing that top level function is utterly trivial. If we tried to do something like that using positional arguments... we would be utterly screwed. Cube requires a completely different argument structure.

Now, one could use options instead of arguments. But for many applications, some of the arguments are not options. You can't describe a cube without all three dimensions!

In the end, I'm not sure how enlightening anyone will find this little discovery. But for my part I'm pleased as punch to have found a new use for something I had written years ago.

For the present application, those extra arguments are being used to feed information to my procedural generator to mark compartments as:

  1. Which layout procedure to use
  2. Compartment usage (if the layout procedure accepts a new usage)
  3. physical dimensions of the new deck
  4. How large to make the compartments
  5. Other math factors that are layout specific

In the deck renderer are several layout strategies to have different goals and different factors for me to fudge. Having an API that allows all of those different layout tools to be triggered by a master procedure is very nice. Especially because after I render the deck, I have another routine (that also accepts dictargs) that takes that rendering computes the polygons, and creates the database records. That procedure uses some of the same arguments as the renderer (like physical height of the deck) but ignores most of the others.

Ok, back to coding.