1. Validated Structs
The purpose of
*struct is to generate
access functions for
validated and type checked
manipulation of data fields stored in a
Tree. Principly, this leverages
static code checking to verify the validity of data accesses.
A *struct definition create
6 access functions
for manipulating data fields.
These functions start with single letter prefixes,
to which is appended _ and structname.
Thus for the
user struct (in the example below) we have:
| Prefix | Description | Signature |
| S | Set/update fields in the record. | proc S_user {t id args} |
| I | Increment an integer or double field. | proc I_user {t id field {amt 1}} |
| G | Get a value from a field. | proc G_user {t id field |
| N | New record instantiation. | proc N_user {t pos args} |
| A | Append a string to a field. | proc A_user {t id field args} |
| L | Lappend a value to a field. | proc L_user {t id field args} |
These so called
S I G N A L functions provide controlled access to data
and can signal warning/errors
at both compile and run time.
2. Example
Structs are particularly useful in conjuction with *tables. Here is an example:
namespace eval ::myapp {
*struct new user {
{ name {} "Name of user" }
{ age 0 "Age of user" -type Int }
{ class {} "List of classes" }
{ prefix {} "String prefix" }
}
*table users {
^ user
bill { "Bill Hale" 29 }
joe { "Joe Black" 19 }
fan { "Fan Tan" 9 }
}
proc DoBad {u id} {
# Do some invalid operations to generate warnings.
puts [G_user $u $id BADKEY]
S_user $u $id age BADVAL
I_user $u $id name
}
proc Main {} {
variable users
set u $users
S_user $u bill name "William Hale" age 8
I_user $u joe age 23.99
A_user $u fan prefix A B C
N_user $u end name "Fred Fay" age 12
if {0} {
DoBad $u bill
}
}
eval Main
}
3. Static Warnings
When above example is run it outputs:
% wize -Wall /tmp/myapp.tcl
/tmp/myapp2.tcl:25: warning: for argument #3 "field", the value
"BADKEY" does not match type <Choice name age class prefix>
for "G_user $u $id BADKEY" in proc [::myapp::DoBad] <types,4>.
/tmp/myapp2.tcl:26: warning: for argument #4 "args", the value
"BADVAL" does not match type <Int> for "S_user $u $id age
BADVAL" in proc [::myapp::DoBad] <types,4>.
/tmp/myapp2.tcl:27: warning: for argument #3 "field", the value
"name" does not match type <Choice age> for "I_user $u $id name"
in proc [::myapp::DoBad] <types,4>.
The interesting thing to
note is that warnings are generated for
code that is not even executed.
Also note that calling I_user on a non int/double
field results in a warning.
See 6.1 Typechecking for details on runtime typechecking.
4. Details
A *struct declaration is a list of lists, each element of
which has up to 3 fixed values:
- name - the name of the field
- init - the initial value for the field
- desc - a comment or description of the field.
optionally follow by name/value pair options:
| Name | Description |
| -label | An optional label |
| -notnull | Field is non-empty and required for N_* |
| -type | The field type eg. Int, Double |
When *struct is called, it creates 6 access functions
in the users namespace.
It also creates an array to stores various other information.
This array may be used by *table.
5. New Records
New records are inserted with a call to N_name, eg.
N_user $u 0 name "Bill Ray" age 10
The pos argument is usually 0.
The record gets initialized with the default values
and then is any passed name/values. Although warnings
will occur, it is not an error to pass in
extra, undefined fields (unless -fixed or -typecheck is true).
Later updates to these undefined fields will also
cause warnings but not errors.
Options can be given to N_ before pos:
| Name | Description |
| -label str | Label to add to node |
| -tags str | List of tags to add |
eg.
N_user $u -label fred end name "Fred Fay" age 12
6. Options
Following are options to *struct:
| Name | Description |
| -evals | Insert eval code via name/value pairs |
| -fixed | Do not allow creation of new fields |
| -pevals | Post eval code via name/value pairs |
| -prefs | Prefixes overrides via name/value pairs |
| -novar | Do not save struct defs in local user array |
| -typecheck | Enforce type strict type checking. |
6.1 Typechecking
By default typechecking consists of simple compile-time advisory
warnings. However
argument types can be strictly enforced at runtime
by setting -typecheck to true,
eg.
*struct new user {
{ First {} "First name of user" }
{ Last {} "Last name of user" }
{ Age 0 "Age of user" -type Int }
} -typecheck 1
*table managers {
^ user
bob { Bob Brown 19 }
tom { Tom Wake 18 }
bill { Bill Williams 17 }
}
$managers update bill Last Barry
S_user $managers bill Age xyz
which results in a runtime error:
for argument #4 "args", the value "xyz" does not match type <Int>
while executing
"S_user $managers bill Age xyz"
(file "mgrs.tcl" line 16)
Note that checking is provided by wize
(via [info checking]). This occurs at
the C level, so runtime overhead is minimal.
6.2 Prefix Override
Lets say you don't like the name prefixes N_ etc,
or you don't
need append, or lappend. The -prefs option
provides a way to override these:
*struct new foo {
{ name {} "A name" }
{ age 0 "The age" }
} -prefs { N New S Set L {} A {} }
set t [tree create]
set id [New_foo $t 0]
Set_foo $t $id name "Larry Flint"
6.3 Adding Code
You can add code to the begin/end of commands using
-evals/-pevals like so:
*struct new foo {
{ name {} "A name" }
{ age 0 "The age" }
} -evals {
I {if {$field == "age" && $amt<0} { error "age < 0"}
A {if {$field == "age"} { error "append age invalid"}
} -pevals {
I {if {$val < 0} { error "value must be >= 0: $val" }
}
set t [tree create]
set id [N_foo $t 0]
I_foo $t $id age -1
Note that this example is contrived, as it would be lower
overhead to just declare the age field as:
*struct new foo {
{ name {} "A name" }
{ age 0 "The age" -type {int -min 0} }
} -typecheck 1
Alternatively, you could just use a write trace.
7. Doing More
Since tree supports variable traces
it is an simple matter to enhance *struct to
do more checking or add integrity constraints.
See the tree man
page for details.
© 2008 Peter MacDonald