Acme-FSM
view release on metacpan or search on metacpan
=cut
=head1 SYNOPSIS
my $bb = Acme::FSM->connect( { %options }, { %fst } );
$bb->process;
exit 0;
=cut
=head1 DESCRIPTION
B<(disclaimer)>
B<Acme::FSM> is currently in proof-of-concept state.
There's a plan to accompany it with B<Acme::FSM::Simple> with all diagnostics
avoided and run time checks in place.
And then with B<Acme::FSM::Tiny> with run time checks stripped.
Also, see L<B<BUGS AND CAVEATS> later in this POD|/BUGS AND CAVEATS>.
=head2 Concerning Inheritance
Through out code only methods are used to access internal state.
Supposedly, that will enable scaling some day later.
The only exception is L<B<connect()>|/connect()> for obvious reasons.
Through out code neither internal state nor FST records are cached ever.
The only one seamingly inconsistent fragment is inside main loop when next
I<$state> is already found but not yet entered.
I<$action> is processed when the next I<$state> is set
(though, in some sense, not yet entered).
(If it doesn't make sense it's fine, refer to
L<B<process()> method|/process()> description later in this POD.)
This notice seems to be useles.
Instead, it might come handy someday.
=cut
=head2 Terminology
There're some weird loaded words in this POD,
most probably, poorly choosen
(but those are going to stay anyway).
So here comes disambiguation list.
=over
=item I<$action>
Special flag associated with next I<{state}> in a I<[turn]>
(covered in details in L<B<process()>|/process()> method description).
B<(note)> It may be applied to a I<[turn]> of I<{fst}> or I<{state}>.
=item blackboard
Shamelesly stolen from B<DMA::FSM>.
Originally, it meant some opaque HASH passed around in B<DMA::FSM::FSM()>
where
user-code could store it's own ideas and such.
Now it's an object blessed into B<Acme::FSM>
(for user-code still, I<{fsm}> has nothing to do with it).
=item entry
Layout of state records (see below) isn't particularly complicated.
However it's far from being uniform.
I<entry> is a placeholder name for any component of (unspecified) state record
when any semantics is irrelevant.
See also I<[turn]> and B<switch()> below.
=item Finite State Machine
B<Acme::FSM>.
=item Finite State Table
=item I<{fst}>
Collection of state records.
=item FSM
Acronym for I<Finite State Machine>.
See above.
=item FST
Acronym for I<Finite State Table>.
See above.
=item internal state
=item I<{fsm}>
Some open-ended set of parameters dictating FSM's behaviour.
=item item
=item I<$item>
Something that makes sense for user-code.
B<Acme::FSM> treats it as scalar with no internals
(mostly;
one exception is covered in L<B<diag()> method|/diag()> description).
=item I<$namespace>
B<A::F> uses elaborate schema to reach various callbacks
(three of them, at the time of writing).
This optional parameter is in use by this schema.
L<B<query()> method|/query()> has more.
=item I<$rule>
A scalar identifing a I<[turn]>.
One of opaque scalars returned by B<switch()> callback
(the other is processed (modified or not) I<$item>).
L<B<process()> method|/process()> description has more.
=item B<source()>
Sets a diagnostic threshold.
It's meaning is covered in L<B<diag()> method|/diag()> documentation.
If C<undef> then set to C<1> (C<0> is B<defined>).
=item I<dumper>
(scalar or C<CODE>)
B<A::F> operates on arbitrary items and there's a diagnostic service that
sometimes insists on somehow showing those arbitrary items.
It's up to user's code to process that arbitrary data and yield some scalar
represantation.
Refer to L<B<query_dumper()> method|/query_dumper()> documentation for
details.
Optional.
Simple stringifier is provided by L<B<query_dumper()> method|/query_dumper()>
itself.
=item I<namespace>
(scalar or object(!) B<ref>)
Sets a context for various parts of I<{fsm}> and services would be resolved.
No defaults.
Refer to L<B<query()> method|/query()> documentation for details.
=item I<source>
(scalar or C<CODE>)
Sets a source of items to process to be queried.
Required.
Refer to L<B<query_source()> method|/query_source()> documentation for
details.
=back
Second is FST (Finite State Table).
It's required for class construction and ignored (if any) for object
construction.
Difference between list and HASH is the former is copied into HASH internally;
the latter HASH is just saved as reference.
The FST is just borrowed from source object during object construction.
Thus, in the synopsis, I<$bb3> and I<$bb2> use references to HASH
I<%$fst2>.
An idea behind this is to minimize memory footprint.
OTOH, maninpulations with one HASH are effectevely manipulations with FST of
any other copy-object that borrowed that FST.
IOW, anything behind FST HASH of class construction or options HASH of object
construction isa trailer and B<carp>ed.
Obviously, there's no trailer in class construction with list FST.
=cut
my @options = qw| diag_level namespace source dumper |;
sub connect {
my( $self, $opts ) = ( shift @_, shift @_ );
ref $opts eq q|HASH| or croak sprintf
q|[connect]: {options} HASH is required, got (%s) instead|, ref $opts;
my( $trailer, $leader );
if( ref $self eq '' ) {
$trailer = ref $_[0] eq q|HASH|;
$self = bless { _ => { fst => $trailer ? shift @_ : { @_ }}}, $self;
$leader = q|clean init with| }
else {
$trailer = @_;
$self = bless { _ => { %{$self->{_}} }}, ref $self;
$leader = q|stealing| }
$self->{_}{$_} = delete $opts->{$_} // $self->{_}{$_} foreach @options;
$self->{_}{diag_level} //= 1;
$self->diag( 3, q|%s (%i) items in FST|,
$leader, scalar keys %{$self->{_}{fst}} );
$self->carp( qq|($_): unknown option, ignored| ) foreach keys %$opts;
$self->carp( q|(source): unset| ) unless defined $self->{_}{source};
$trailer = ref $_[0] eq q|HASH| ? keys %{$_[0]} : @_ if $trailer;
$self->carp( qq|ignoring ($trailer) FST items in trailer| ) if $trailer;
$self->carp( q|FST has no {START} state| ) unless
exists $self->{_}{fst}{START};
$self->carp( q|FST has no {STOP} state| ) unless
exists $self->{_}{fst}{STOP};
@{$self->{_}}{qw| state action |} = qw| START VOID |;
return $self }
=head1 B<process()>
$bb = Acme::FSM->connect( { }, \%fst );
$rc = $bb->process;
This is heart, brains, liver, and so on of B<Acme::FSM>.
B<process()> is what does actual state flow.
That state flow is steered by records in I<%fst>.
Each record consists of:
=over
=item B<switch()> callback
This is some user supplied code that
always consumes whatever B<source()> callback returns (at some point in past),
(optionally) regurgitates said input,
and decides what I<[turn]> to go next.
Except when in special I<{state}> (see below) it's supplied with one argument:
I<$item>.
In special states I<$item> is missing.
B<switch()> returns two controls: I<$rule> and processed I<$item>.
What to do with returned I<$item> is determined by I<$action> (see below).
=item various I<[turn]>s
Each I<[turn]> spec is an ARRAY with two elements (trailing elements are
ignored).
First element is I<$state> to change to.
Second is I<$action> that sets what to do with I<$item> upon changing to named
I<$state>.
Here are known I<[turn]>s in order of logical treating decreasing.
=over
=item C<eturn>
That I<[turn]> will be choosen by FSM itself when I<$item> at hands is
C<undef>
(as returned by B<source()>).
I<switch()> isn't invoked -- I<$item> is void, there's nothing to call
I<switch()> with.
However, I<$action> may change I<$item>.
my $item = shift;
$item % 5 ? !0 : !1, $item
},
tturn => [ qw/ delta SAME / ],
fturn => [ qw/ bravo_baz SAME / ]
},
bravo_baz =>
{
switch => sub {
my $item = shift;
$item % 3 ? !0 : !1, $item
},
tturn => [ qw/ echo SAME / ],
fturn => [ qw/ bravo NEXT / ]
}
World of a difference.
Furthermore, if you dare, take a look at
F<t/process/quadratic.t> and F<t/process/sort.t>.
But better don't, those are horrible.
Now, illustration what I<$namespace> configuration parameter opens.
Classics:
my $bb = Acme::FSM->connect(
{ },
alpha => {
switch => sub {
shift % 42, ''
}
}
);
Illustrations below are heavily stripped for brevity
(Perlwise and B<A::F>wise).
Separate namespace:
package Secret::Namespace;
sub bravo_sn {
shift;
shift % 42, ''
}
package Regular::Namespace;
use Acme::FSM;
my $bb = Acme::FSM->connect(
{ namespace => 'Secret::Namespace' },
alpha => {
switch => 'bravo_sn'
}
);
Separate class:
package Secret::Class;
sub new { bless { }, shift }
sub bravo_sc {
shift;
shift % 42, ''
}
package Regular::Class;
use Secret::Class;
use Acme::FSM;
my $not_bb = Secret::Class->new;
my $bb = Acme::FSM->connect(
{ namespace => $not_bb },
alpha => {
switch => 'bravo_sc'
}
);
And, finally, B<A::F> implodes upon itself:
package Secret::FSM;
use parent qw/ Acme::FSM /;
sub connect {
my $class = shift;
my $bb = $class->SUPER::connect( @_ );
} # or just skip constructor, if not needed
sub bravo_sf {
shift;
shift % 42, ''
}
package main;
my $bb = Secret::FSM->connect(
{ namespace => '' },
alpha => {
switch => 'bravo_sf'
}
);
=cut
=head1 COPYRIGHT AND LICENSE
Original Copyright: 2008 Dale M Amon. All rights reserved.
Original License:
LGPL-2.1
Artistic
BSD
Original Code Acquired from:
http://www.cpan.org/authors/id/D/DA/DALEAMON/DMAMisc-1.01-3.tar.gz
Copyright 2012, 2013, 2022 Eric Pozharski <whynto@pozharski.name>
Copyright 2025 Eric Pozharski <wayside.ultimate@tuta.io>
GNU LGPLv3
( run in 3.501 seconds using v1.01-cache-2.11-cpan-39bf76dae61 )