As an addendum to yesterday’s little writeup on data-driven design, here’s another little (perl) trick I’m particularly fond of, that relies on storing perl code as a hash element. The cool thing about this approach is that you can actually have a configuration file written in, say, XML, that can then reference procedures which are actually defined in the configuration along with the rest of the parameters — allowing you to abstract it away from the core application 100%.
Here is a sample configuration file, for an engine I’m developing for other purposes. The engine itself does a bunch of things on it’s own, evaluating the existence of required configuration options, existence and executability of the function pointed to by the “entry” option, including requisite modules and external perl files, etc., but the crux of the engine is it’s ability to execute the procedures stored under the ‘procedures’ element of the configuration. Each procedure can set $state to a new value; the core engine then attempts to execute the procedure whose name matches $state; thus making it possible to build a dynamic state engine very quickly, and in such a fashion that the state engine can modify itself by adding/changing/removing elements of the ‘procedures’ portion of the configuration hash:
1
2 %ise = (
3 'version' => '1.0',
4 'name' => 'Test Engine',
5 'entry' => 'init',
6 'requirements' => [
7 'options:libs',
8 'options:modules',
9 ],
10 'options' => {
11 'output' => 1,
12 # libs defines the search-path for extra modules loaded below
13 'libs' => [
14 '/home/mholger/scripts',
15 ],
16 # Custom / extra modules required by the engine
17 'modules' => [
18 'IO::Socket',
19 ],
20 'externs' => [
21 'hpfile.pl',
22 ],
23 },
24 'procedures' => {
25 # 'init' is the only mandatory procedure (as defined by 'entry', above)
26 # The rest is up to you.
27 'init' => q{
28 print "Hello World!\n";
29 },
30 },
31 );
And here we have just the snippet of code from the engine itself responsible for executing the procedures defined in the configuration hash above (note, this *is* a completely different file from the code snippet above, so line numbers are purely for reference):
127 # Begin Core State Engine
128 while( $state ne '' && exists( $ise{'procedures'}->{$state} ))
129 {
130 my $curstate = $state;
131 $startTime = time();
132
133 eval $ise{'procedures'}->{$state};
134 if( $@ ) # Eval sets $@ if an error occurs
135 {
136 my @cmd = split( /\n/, $ise{'procedures'}->{$state} );
137 my $errline = "";
138 while( $@ =~ /line (\d+)/g )
139 {
140 $errline .= ":$1:";
141 }
142 for( my $xx = 0; $xx < $#cmd; $xx++ )
143 {
144 my $errtoken = '';
145 my $errloc = $xx + 1;
146 if( $errline =~ /:$errloc:/ )
147 {
148 $errtoken = '*';
149 }
150 $cmd[$xx] = sprintf( "\t%1s%03i: $cmd[$xx]\n", $errtoken, $xx + 1 );
151 }
152 $ise{'procedures'}->{$state} = join( '', @cmd );
153 die "\n\n" . '#' x 75 . "\nWhile executing procedure '$state':\n$@\nStack:\n" . $ise{'procedures'}->{$state} . "\n" . '#' x 75 . "\n\n";
154 }
155 $endTime = time() - $startTime;
156 $timeCumul += $endTime;
157 $stepTimes->[$stepnum]->{'runstate'} = $curstate;
158 $stepTimes->[$stepnum]->{'nextstate'} = $state;
159 $stepTimes->[$stepnum]->{'runtime'} = $endTime;
160 $stepnum++;
161 $state_env{'prevstate'} = $curstate;
162 }
163 # End State Engine
Note that the execution occurs on line 133, and the while loop opened on line 128 and closed on line 162 are all that is necessary for this “program” to function ad nauseum; the rest of the code provides for generous error handling and performance statistics gathering.
The really cool thing here is that the %ise hash that is being used is just a data structure in memory! How it gets there is entirely up to the user.
Also, note the ability of a procedure to do crazy things by adding/modifying $main::ise{‘procedures’} — since everything in %ise is just data, you can operate on it — even the code stored in the ‘procedures’ element, just like any other data. All sorts of fun!
