Spiga

More on Code as Data as Code

June 05, 08 by M. Holger

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!

This entry no have comments... but you can be first.

Leave a Reply

You must be logged in to post a comment.