Name

FSA::Rules - Build simple rules-based state machines in Perl

Synopsis

my $fsa = FSA::Rules->new(
   ping => {
       do => sub {
           print "ping!\n";
           my $state = shift;
           $state->result('pong');
           $state->machine->{count}++;
       },
       rules => [
           game_over => sub { shift->machine->{count} >= 20 },
           pong      => sub { shift->result eq 'pong' },
       ],
   },

   pong => {
       do => sub { print "pong!\n" },
       rules => [ ping => 1, ], # always goes back to ping
   },
   game_over => { do => sub { print "Game Over\n" } }
);

$fsa->start;
$fsa->switch until $fsa->at('game_over');

Description

This class implements a simple state machine pattern, allowing you to quickly build rules-based state machines in Perl. As a simple implementation of a powerful concept, it differs slightly from an ideal DFA model in that it does not enforce a single possible switch from one state to another. Rather, it short circuits the evaluation of the rules for such switches, so that the first rule to return a true value will trigger its switch and no other switch rules will be checked. (But see the strict attribute and parameter to new().) It differs from an NFA model in that it offers no back-tracking. But in truth, you can use it to build a state machine that adheres to either model--hence the more generic FSA moniker.

FSA::Rules uses named states so that it's easy to tell what state you're in and what state you want to go to. Each state may optionally define actions that are triggered upon entering the state, after entering the state, and upon exiting the state. They may also define rules for switching to other states, and these rules may specify the execution of switch-specific actions. All actions are defined in terms of anonymous subroutines that should expect an FSA::State object itself to be passed as the sole argument.

FSA::Rules objects and the FSA::State objects that make them up are all implemented as empty hash references. This design allows the action subroutines to use the FSA::State object passed as the sole argument, as well as the FSA::Rules object available via its machine() method, to stash data for other states to access, without the possibility of interfering with the state or the state machine itself.

Serialization

As of version 0.24, FSA::Rules supports serialization by Storable 2.05 and later. In other words, FSA::Rules can function as a persistent state machine.

However, FSA::Rules stores data outside of FSA::Rules objects, in private data structures inside the FSA::Rules module itself. Therefore, unless you want to clone your FSA::Rules object, you must let it fall out of scope after you serialize it, so that its data will be cleared from memory. Otherwise, if you freeze and thaw an FSA::Rules object in a single process without undefing the original, there will be two copies of the object stored by FSA::Rules.

So how does it work? Because the rules are defined as code references, you must use Storable 2.05 or later and set its $Deparse and $Eval variables to true values:

use Storable 2.05 qw(freeze thaw);

local $Storable::Deparse = 1;
local $Storable::Eval    = 1;

my $frozen = freeze($fsa);
$fsa = thaw($frozen);

The only caveat is that, while Storable can serialize code references, it doesn't properly reference closure variables. So if your rules code references are closures, you'll have to serialize the data that they refer to yourself.

Class Interface

Constructor

new

my $fsa = FSA::Rules->new(
    foo_state => { ... },
    bar_state => { ... },
);

$fsa = FSA::Rules->new(
    \%params,
    foo_state => { ... },
    bar_state => { ... },
);

Constructs and returns a new FSA::Rules object. An optional first argument is a hash reference that may contain one or more of these keys:

start

Causes the start() method to be called on the machine before returning it.

done

A value to which to set the done attribute.

strict

A value to which to set the strict attribute.

state_class

The name of the class to use for state objects. Defaults to "FSA::State". Use this parameter if you want to use a subclass of FSA::State.

state_params

A hash reference of parameters to pass as a list to the state_class constructor.

All other parameters define the state table, where each key is the name of a state and the following hash reference defines the state, its actions, and its switch rules. These state specifications will be converted to FSA::State objects available via the states() method. The first state parameter is considered to be the start state; call the start() method to automatically enter that state.

The supported keys in the state definition hash references are:

label
label => 'Do we have a username?',
label => 'Create a new user',

A label for the state. It might be the question that is being asked within the state (think decision tree), the answer to which determines which rule will trigger the switch to the next state. Or it might merely describe what's happening in the state.

on_enter
on_enter => sub { ... }
on_enter => [ sub {... }, sub { ... } ]

Optional. A code reference or array reference of code references. These will be executed when entering the state, after any switch actions defined by the rules of the previous state. The FSA::State for which the on_enter actions are defined will be passed to each code reference as the sole argument.

do
do => sub { ... }
do => [ sub {... }, sub { ... } ]

Optional. A code reference or array reference of code references. These are the actions to be taken while in the state, and will execute after any on_enter actions. The FSA::State object for which the do actions are defined will be passed to each code reference as the sole argument.

on_exit
on_exit => sub { ... }
on_exit => [ sub {... }, sub { ... } ]

Optional. A code reference or array reference of code references. These will be executed when exiting the state, before any switch actions (defined by rules). The FSA::State object for which the on_exit actions are defined will be passed to each code reference as the sole argument.

rules

Optional. The rules for switching from the state to other states. This is an array reference but shaped like a hash. The keys are the names of the states to consider moving to, while the values are the rules for switching to that state. The rules will be executed in the order specified in the array reference, and they will short-circuit unless the strict attribute has been set to a true value. So for the sake of efficiency it's worthwhile to specify the switch rules most likely to evaluate to true before those more likely to evaluate to false.

Rules themselves are best specified as hash references with the following keys:

rule

A code reference or value that will be evaluated to determine whether to switch to the specified state. The value must be true or the code reference must return a true value to trigger the switch to the new state, and false not to switch to the new state. When executed, it will be passed the FSA::State object for the state for which the rules were defined, along with any other arguments passed to try_switch() or switch()--the methods that execute the rule code references. These arguments may be inputs that are specifically tested to determine whether to switch states. To be polite, rules should not transform the passed values if they're returning false, as other rules may need to evaluate them (unless you're building some sort of chaining rules--but those aren't really rules, are they?).

message

An optional message that will be added to the current state when the rule specified by the rule parameter evaluates to true. The message will also be used to label switches in the output of the graph() method.

action

A code reference or an array reference of code references to be executed during the switch, after the on_exit actions have been executed in the current state, but before the on_enter actions execute in the new state. Two arguments will be passed to these code references: the FSA::State object for the state for which they were defined, and the FSA::State object for the new state (which will not yet be the current state).

A couple of examples:

rules => [
    foo => {
        rule => 1
    },
    bar => {
        rule    => \&goto_bar,
        message => 'Have we got a bar?',
    },
    yow => {
        rule    => \&goto_yow,
        message => 'Yow!',
        action  => [ \&action_one, \&action_two],
    }
]

A rule may also simply be a code reference or value that will be evaluated when FSA::Rules is determining whether to switch to the new state. You might want just specify a value or code reference if you don't need a message label or switch actions to be executed. For example, this rules specification:

rules => [
    foo => 1
]

Is equivalent to this rules specification:

rules => [
    foo => { rule => 1 }
]

And finally, you can specify a rule as an array reference. In this case, the first item in the array will be evaluated to determine whether to switch to the new state, and any other items must be code references that will be executed during the switch. For example, this rules specification:

rules => [
    yow => [ \&check_yow, \&action_one, \&action_two ]
]

Is equivalent to this rules specification:

rules => [
    yow => {
        rule   =>  \&check_yow,
        action => [ \&action_one, \&action_two ],
    }
]

Instance Interface

Instance Methods

start

my $state = $fsa->start;

Starts the state machine by setting the state to the first state defined in the call to new(). If the machine is already in a state, an exception will be thrown. Returns the start state FSA::State object.

at

$fsa->switch until $fsa->at('game_over');

Requires a state name. Returns false if the current machine state does not match the name. Otherwise, it returns the state.

curr_state

my $curr_state = $fsa->curr_state;
$fsa->curr_state($curr_state);

Get or set the current FSA::State object. Pass a state name or object to set the state. Setting a new state will cause the on_exit actions of the current state to be executed, if there is a current state, and then execute the on_enter and do actions of the new state. Returns the new FSA::State object when setting the current state.

state

Deprecated alias for curr_state(). This method will issue a warning and will be removed in a future version of FSA::Rules. Use curr_state(), instead.

prev_state

my $prev_state = $fsa->prev_state;

Returns the FSA::State object representing the previous state. This is useful in states where you need to know what state you came from, and can be very useful in "fail" states.

states

my @states = $fsa->states;
my $states = $fsa->states;
my $state  = $fsa->states($state_name);
@states    = $fsa->states(@state_names);
$states    = $fsa->states(@state_names);

Called with no arguments, this method returns a list or array reference of all of the FSA::State objects that represent the states defined in the state machine. When called with a single state name, it returns the FSA::State object object for that state. When called with more than one state name arguments, it returns a list or array reference of those states.

If called with any state names that did not exist in the original definition of the state machine, this method will croak().

try_switch

my $state = $fsa->try_switch;
$state = $fsa->try_switch(@inputs);

Checks the switch rules of the current state and switches to the first new state for which a rule returns a true value. The evaluation of switch rules short-circuits to switch to the first state for which a rule evaluates to a true value unless the strict attribute is set to a true value.

If <strict> is set to a true value, all rules will be evaluated, and if more than one returns a true statement, an exception will be thrown. This approach guarantees that every attempt to switch from one state to another will have one and only one possible destination state to which to switch, thus satisfying the DFA pattern.

All arguments passed to try_switch will be passed to the switch rule code references as inputs. If a switch rule evaluates to true and there are additional switch actions for that rule, these actions will be executed after the on_exit actions of the current state (if there is one) but before the on_enter actions of the new state. They will be passed the current state object and the new state object as arguments.

Returns the FSA::State object representing the state to which it switched and undef if it cannot switch to another state.

switch

my $state = eval { $fsa->switch(@inputs) };
print "No can do" if $@;

The fatal form of try_switch(). This method attempts to switch states and returns the FSA::State object on success and throws an exception on failure.

done

my $done = $fsa->done;
$fsa->done($done);
$fsa->done( sub {...} );

Get or set a value to indicate whether the engine is done running. Or set it to a code reference to have that code reference called each time done() is called without arguments and have its return value returned. A code reference should expect the FSA::Rules object passed in as its only argument. Note that this varies from the pattern for state actions, which should expect the relevant FSA::State object to be passed as the argument. Call the curr_state() method on the FSA::Rules object if you want the current state in your done code reference.

This method can be useful for checking to see if your state engine is done running, and calling switch() when it isn't. States can set it to a true value when they consider processing complete, or you can use a code reference that determines whether the machine is done. Something like this:

my $fsa = FSA::Rules->new(
    foo => {
        do    => { $_[0]->machine->done(1) if ++$_[0]->{count} >= 5 },
        rules => [ foo => 1 ],
    }
);

Or this:

my $fsa = FSA::Rules->new(
    foo => {
        do    => { ++shift->machine->{count} },
        rules => [ foo => 1 ],
    }
);
$fsa->done( sub { shift->{count} >= 5 });

Then you can just run the state engine, checking done() to find out when it's, uh, done.

$fsa->start;
$fsa->switch until $fsa->done;

Although you could just use the run() method if you wanted to do that.

Note that done will be reset to undef by a call to reset() when it's not a code reference. If it is a code reference, you need to be sure to write it in such a way that it knows that things have been reset (by examining states, for example, all of which will have been removed by reset()).

strict

my $strict = $fsa->strict;
$fsa->strict(1);

Get or set the strict attribute of the state machine. When set to true, the strict attribute disallows the short-circuiting of rules and allows a transfer if only one rule returns a true value. If more than one rule evaluates to true, an exception will be thrown.

run

$fsa->run;

This method starts the FSA::Rules engine (if it hasn't already been set to a state) by calling start(), and then calls the switch() method repeatedly until done() returns a true value. In other words, it's a convenient shortcut for:

$fsa->start unless $self->curr_state;
$fsa->switch until $self->done;

But be careful when calling this method. If you have no failed switches between states and the states never set the done attribute to a true value, then this method will never die or return, but run forever. So plan carefully!

Returns the FSA::Rules object.

reset

$fsa->reset;

The reset() method clears the stack and notes, sets the current state to undef, and sets done to undef (unless done is a code reference). Also clears any temporary data stored directly in the machine hash reference and the state hash references. Use this method when you want to reuse your state machine. Returns the DFA::Rules object.

my $fsa = FSA::Rules->new(@state_machine);
$fsa->done(sub {$done});
$fsa->run;
# do a bunch of stuff
$fsa->{miscellaneous} = 42;
$fsa->reset->run;
# $fsa->{miscellaneous} does not exist

notes

$fsa->notes($key => $value);
my $val = $fsa->notes($key);
my $notes = $fsa->notes;

The notes() method provides a place to store arbitrary data in the state machine, just in case you're not comfortable using the FSA::Rules object itself, which is an empty hash. Any data stored here persists for the lifetime of the state machine or until reset() is called.

Conceptually, notes() contains a hash of key-value pairs.

$fsa->notes($key => $value) stores a new entry in this hash. $fsa->notes->($key) returns a previously stored value. $fsa->notes, called without arguments, returns a reference to the entire hash of key-value pairs.

Returns the FSA::Rules object when setting a note value.

last_message

my $message = $fsa->last_message;
$message = $fsa->last_message($state_name);

Returns the last message of the current state. Pass in the name of a state to get the last message for that state, instead.

last_result

my $result = $fsa->last_result;
$result = $fsa->last_result($state_name);

Returns the last result of the current state. Pass in the name of a state to get the last result for that state, instead.

stack

my $stack = $fsa->stack;

Returns an array reference of all states the machine has been in since it was created or since reset() was last called, beginning with the first state and ending with the current state. No state name will be added to the stack until the machine has entered that state. This method is useful for debugging.

raw_stacktrace

my $stacktrace = $fsa->raw_stacktrace;

Similar to stack(), this method returns an array reference of the states that the machine has been in. Each state is an array reference with two elements. The first element is the name of the state and the second element is a hash reference with two keys, "result" and "message". These are set to the values (if used) set by the result() and message() methods on the corresponding FSA::State objects.

A sample state:

[
    some_state,
    {
        result  => 7,
        message => 'A human readable message'
    }
]

stacktrace

my $trace = $fsa->stacktrace;

Similar to raw_stacktrace, except that the results and messages are output in a human readable format with nicely formatted data (using Data::Dumper). Functionally there is no difference from raw_stacktrace() unless your states are storing references in their results or messages

For example, if your state machine ran for only three states, the output may resemble the following:

State: foo
{
  message => 'some message',
  result => 'a'
}

State: bar
{
  message => 'another message',
  result => [0, 1, 2]
}

State: bar
{
  message => 'and yet another message',
  result => 2
}

graph

my $graph_viz = $fsa->graph(@graph_viz_args);
$graph_viz = $fsa->graph(\%params, @graph_viz_args);

Constructs and returns a GraphViz object useful for generating graphical representations of the complete rules engine. The parameters to graph() are all those supported by the GraphViz constructor; consult the GraphViz documentation for details.

Each node in the graph represents a single state. The label for each node in the graph will be either the state label or if there is no label, the state name.

Each edge in the graph represents a rule that defines the relationship between two states. If a rule is specified as a hash reference, the message key will be used as the edge label; otherwise the label will be blank.

An optional hash reference of parameters may be passed as the first argument to graph(). The supported parameters are:

with_state_name

This parameter, if set to true, prepends the name of the state and two newlines to the label for each node. If a state has no label, then the state name is simply used, regardless. Defaults to false.

wrap_nodes
wrap_node_labels

This parameter, if set to true, will wrap the node label text. This can be useful if the label is long. The line length is determined by the wrap_length parameter. Defaults to false.

wrap_edge_labels
wrap_labels

This parameter, if set to true, will wrap the edge text. This can be useful if the rule message is long. The line length is determined by the wrap_length parameter. Defaults to false wrap_labels is deprecated and will be removed in a future version.

text_wrap
wrap_length

The line length to use for wrapping text when wrap_nodes or wrap_labels is set to true. text_wrap is deprecated and will be removed in a future version. Defaults to 25.

node_params

A hash reference of parameters to be passed to the GraphViz add_node() method when setting up a state as a node. Only the label parameter will be ignored. See the GraphViz|GraphViz/"add_node" documentation for the list of supported parameters.

edge_params

A hash reference of parameters to be passed to the GraphViz add_node() method when setting up a state as a node. See the GraphViz|GraphViz/"add_edge" documentation for the list of supported parameters.

Note: If either GraphViz or Text::Wrap is not available on your system, graph() will simply will warn and return.

DESTROY

This method cleans up an FSA::Rules object's internal data when it is released from memory. In general, you don't have to worry about the DESTROY() method unless you're subclassing FSA::Rules. In that case, if you implement your own DESTROY() method, just be sure to call SUPER::DESTROY() to prevent memory leaks.

FSA::State Interface

FSA::State objects represent individual states in a state machine. They are passed as the first argument to state actions, where their methods can be called to handle various parts of the processing, set up messages and results, or access the state machine object itself.

Like FSA::Rules objects, FSA::State objects are empty hashes, so you can feel free to stash data in them. But note that each state object is independent of all others, so if you want to stash data for other states to access, you'll likely have to stash it in the state machine object (in its hash implementation or via the notes() method), or retrieve other states from the state machine using its states() method and then access their hash data directly.

Constructor

new

my $state = FSA::State->new;

Constructs and returns a new FSA::State object. Not intended to be called directly, but by FSA::Rules.

Instance Methods

name

my $name = $state->name;

Returns the name of the state.

label

my $label = $state->label;

Returns the label of the state.

machine

my $machine = $state->machine;

Returns the FSA::Rules object for which the state was defined.

result

my $fsa = FSA::Rules->new(
  # ...
  some_state => {
      do => sub {
          my $state = shift;
          # Do stuff...
          $state->result(1); # We're done!
      },
      rules => [
          bad  => sub { ! shift->result },
          good => sub {   shift->result },
      ]
  },
  # ...
);

This is a useful method to store results on a per-state basis. Anything can be stored in the result slot. Each time the state is entered, it gets a new result slot. Call result() without arguments in a scalar context to get the current result; call it without arguments in an array context to get all of the results for the state for each time it has been entered into, from first to last. The contents of each result slot can also be viewed in a stacktrace or raw_stacktrace.

message

my $fsa = FSA::Rules->new(
  # ...
  some_state => {
      do => sub {
          my $state = shift;
          # Do stuff...
          $state->message('hello ', $ENV{USER});
      },
      rules => [
          bad  => sub { ! shift->message },
          good => sub {   shift->message },
      ]
  },
  # ...
);

This is a useful method to store messages on a per-state basis. Anything can be stored in the message slot. Each time the state is entered, it gets a new message slot. Call message() without arguments in a scalar context to get the current message; call it without arguments in an array context to get all of the messages for the state for each time it has been entered into, from first to last. The contents of each message slot can also be viewed in a stacktrace or raw_stacktrace.

prev_state

my $prev = $state->prev_state;

A shortcut for $state->machine->prev_state.

done

my $done = $state->done;
$state->done($done);

A shortcut for $state->machine->done. Note that, unlike message and result, the done attribute is stored machine-wide, rather than state-wide. You'll generally call it on the state object when you want to tell the machine that processing is complete.

notes

my $notes = $state->notes;
$state->notes($notes);

A shortcut for $state->machine->notes. Note that, unlike message and result, notes are stored machine-wide, rather than state-wide. It is therefore probably the most convenient way to stash data for other states to access.

enter

Executes all of the on_enter actions. Called by FSA::Rules's curr_state() method, and not intended to be called directly.

do

Executes all of the do actions. Called by FSA::Rules's curr_state() method, and not intended to be called directly.

exit

Executes all of the on_exit actions. Called by FSA::Rules's curr_state() method, and not intended to be called directly.

DESTROY

This method cleans up an FSA::State object's internal data when it is released from memory. In general, you don't have to worry about the DESTROY() method unless you're subclassing FSA::State. In that case, if you implement your own DESTROY() method, just be sure to call SUPER::DESTROY() to prevent memory leaks.

To Do

Factor FSA::Class into a separate file.
Switch to Data::Dump::Streamer for proper serialization of closures.

Support

This module is stored in an open GitHub repository. Feel free to fork and contribute!

Please file bug reports via GitHub Issues or by sending mail to bug-FSA-Rules@rt.cpan.org.

Authors

David E. Wheeler <david@justatheory.com>
Curtis "Ovid" Poe <eop_divo_sitruc@yahoo.com> (reverse the name to email him)

Copyright and License

Copyright (c) 2004-2015 David E. Wheeler. Some Rights Reserved.

This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself.