The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

Sub::Genius - module for managing concurrent Perl semantics in the uniprocess execution model of perl.

Another way to say this, is that it introduces all the joys and pains of multi-threaded, shared memory programming to the uniprocess environment that is perl.

One final way to say it, if we're going to fake the funk out of coroutines [1], let's do it correctly. :)

THIS MODULE IS EXPERIMENTAL

Until further noted, this module subject to extreme fluxuations in interfaces and implied approaches. The hardest part about this will be managing all the cool and bright ideas stemming from it.

STATIC CODE STUB GENERATION TOOL

Eventually this module will install a tool called stubby into your local $PATH. For the time being it is located in the ./bin directory of the distribution and on Github.

SYNOPSIS

    my $pre = q{( A B    &     C D )       Z};
    #             \ /          \ /         |
    #>>>>>>>>>>>  L1  <shuff>  L2   <cat>  L3

    my $sq = Sub::Genius->new(pre => $pre);

    # sub declaration order has no bearing on anything
    sub A { print qq{A} }
    sub B { print qq{B} }
    sub C { print qq{C} }
    sub D { print qq{D} }
    sub Z { print qq{\n}}

    $sq->run_once();
    print qq{\n};

The following expecity execution of the defined subroutines are all valid according to the PRE description above:

    # valid order 1
      A(); B(); C(); D(); Z();
    
    # valid order 2
      A(); C(); B(); D(); Z();
    
    # valid order 3
      A(); C(); D(); B(); Z();
    
    # valid order 4
      C(); A(); D(); B(); Z();
    
    # valid order 5
      C(); D(); A(); B(); Z();

In the example above, using a PRE to describe the relationship among subroutine names (these are just multicharacter symbols); we are expressing the following constraints:

sub A must run before sub B
sub C must run before sub D
sub Z is always called last

Sub::Genius uses FLAT's functionality to generate any of a number of valid "strings" or correct symbol orderings that are accepted by the Regular Language described by the shuffle two regular languages.

Meaningful Subroutine Names

FLAT allows single character symbols to be expressed with out any decorations;

    my $pre = q{ s ( A (a b) C & D E F) f };

The concatentaion of single symbols is implied, and spaces between symbols doesn't even matter. The following is equivalent to the PRE above,

    my $pre = q{s(A(ab)C&DEF)f};

It's important to note immediately after the above example, that the PRE may contain symbols that are made up of more than one character. This is done using square brackets ([...]), e.g.:

    my $pre = q{[s]([A]([a][b])[C]&[D][E][F])[f]};

But this is a mess, so we can use longer subroutine names as symbols and break it up in a more familar way:

    my $pre = q{
      [start]
        (
          [sub_A]
          (
            [sub_a]
            [sub_b]
          )
          [sub_C]
        &
          [sub_D]
          [sub_E]
          [sub_F]
        )
      [fin]
    };

This is much nicer and starting to look like a more natural expression of concurrent semantics.

PERL's UNIPROCESS MEMORY MODEL AND ITS EXECUTION ENVIRONMENT

While the language Perl is not necessarily constrained by a uniprocess execution model, the runtime provided by perl is. This has necessarily restricted the expressive semantics that can very easily be extended to DWIM in a concurrent execution model. The problem is that perl has been organically grown over the years to run as a single process. It is not immediately obvious to many, even seasoned Perl programmers, why after all of these years does perl not have real threads or admit real concurrency and semantics. Accepting the truth of the uniprocess model makes it clear and brings to it a lot of freedom. This module is meant to facilitate shared memory, multi-process reasoning to perl's fixed uniprocess reality.

Atomics and Barriers

When speaking of concurrent semantics in Perl, the topic of atomic primatives often comes up, because in a truly multi-process execution environment, they are very important to coordinating the competitive access of resources such as files and shared memory. Since this execution model necessarily serializes parallel semantics in a sequentially consistent way, there is no need for any of these things. Singular lines of execution need no coordination because there is no competition for any resource (e.g., a file, memory, network port, etc).

RUNTIME METHODS

A minimal set of methods is provided, more so to not suggest the right way to use this module.

new

Constructor, requires a single scalar string argument that is a valid PRE accepted by FLAT.

    my $pre = q{
      [start]
        (
          [subA] (
            [subB_a] [subB_b]
          ) [subC]
        &
          [subD] [subE] [subF]
        )
      [finish]
    };

    my $sq = Sub::Genius->new(pre => $pre);

Note: due to the need to explore the advantages of supporting infinite languages, i.e., PREs that contain a Kleene star; init_plan will die after it compiles the PRE into a min DFA. It checks this using the FLAT::DFA::is_finite subroutine, which simply checks for the presence of cycles. Once this is understood more clearly, this restriction may be lifted. This module is all about correctness, and only finite languages are being considered at this time.

The reference, if captured by a scalar, can be wholly reset using the same parameters as new but calling the plan_nein methods. It's a minor convenience, but one all the same.

plan_nein

Using an existing reference instantiation of Sub::Genius, resets everything about the instance. It's effectively link calling new on the instance without having to recapture it.

init_plan

This takes the PRE provided in the new constructure, and runs through the conversion process provded by FLAT to an equivalent mininimzed DFA. It's this DFA that is then used to generate the (currently) finite set of strings, or plans that are acceptible for the algorithm or steps being implemented.

    my $pre = q{
      [start]
        (
          [subA] (
            [subB_a] [subB_b]
          ) [subC]
        &
          [subD] [subE] [subF]
        )
      [finish]
    };
    my $sq = Sub::Genius->new(pre => $pre);
    $sq->init_plan;
run_once

Returns scope as affected by the assorted subroutines.

Accepts two parameters, both are optional:

ns => q{My::Subsequentializer::Oblivious::Funcs}

Defaults to main::, allows one to specify a namespace that points to a library of subroutines that are specially crafted to run in a sequentialized environment. Usually, this points to some sort of willful obliviousness, but might prove to be useful nonetheless.

scope => {}

Allows one to initiate the execution scoped memory, and may be used to manage a data flow pipeline. Useful and consistent only in the context of a single plan execution. If not provided, scope is initialized as an empty anonymous hash reference:

    my $final_scope = $sq->run_once(
                           scope   => {},
                           verbose => undef,
                      );
verbose => 1|0

Default is falsy, or off. When enabled, outputs arguably useless diagnostic information.

Runs the execution plan once, returns whatever the last subroutine executed returns:

    my $pre = join(q{&},(a..z));
    my $sq = Sub::Genius->new(pre => $pre);
    $plan = $sq->init_plan;
    my $final_scope = $sq->run_once;
next

FLAT provides some utility methods to pump FAs for valid strings; effectively, its the enumeration of paths that exist from an initial state to a final state. There is nothing magical here. The underlying method used to do this actually creates an interator.

When next is called the first time, an interator is created, and the first string is returned. There is currently no way to specify which string (or plan) is returned first, which is why it is important that the concurrent semantics declared in the PRE are done in such a way that any valid string presented is considered to be sequentially consistent with the memory model used in the implementation of the subroutines. Perl provides the access to these memories by use of their lexical variable scoping (my, local) and the convenient way it allows one to make a subroutine maintain persistent memory (i.e., make it a coroutine) using the state keyword. See more about PERL's UNIPROCESS MEMORY MODEL AND ITS EXECUTION ENVIRONMENT in the section above of the same name.

An example of iterating over all valid strings in a loop follows:

    while (my $plan = $sq->next_plan()) {
      print qq{Plan: $plan\n};
      $sq->run_once;
    }

Note, in the above example, the concept of pipelining is violated since the loop is running each plan ( with no guaranteed ordering ) in turn. $scope is only meaningful within each execution context. Dealing with multiple returned final scopes is not part of this module, but can be captured during each iteration for future processessing:

    my @all_final_scopes = ();
    while (my $plan = $sq->next_plan()) {
      print qq{Plan: $plan\n};
      my $final_scope = $sq->run_once;
      push @all_final_scopes, { $plan => $final_scope };
    }
    # now do something with all the final scopes collected
    # by @all_final_scopes

At this time Sub::Genius only permits finite languages, therefore there is always a finite list of accepted strings. The list may be long, but it's finite.

As an example, the following admits a large number of orderings in a realtively compact DFA, in fact there are 26! (factorial) such valid orderings:

    my $pre = join(q{&},(a..z));
    my $final_scope = Sub::Genius->new(pre => $pre)->run_once;

Thus, the following will take long time to complete; but it will complete:

    my $ans; # global to all subroutines executed
    while ($my $plan = $sq->next_plan()) {
      $sq->run_once;
    }
    print qq{ans: $ans\n};

Done right, the output after 26! iterations may very well be:

    ans: 42

A formulation of 26 subroutines operating over shared memory in which all cooperative execution of all 26! orderings reduces to 42 is left as an excercise for the reader.

run_any

For convenience, this wraps up the steps of plan, init_plan, next, and run_once. It presents a simple one line interfaces:

    my $pre = q{
      [start]
        (
          [subA] (
            [subB_a] [subB_b]
          ) [subC]
        &
          [subD] [subE] [subF]
        )
      [finish]
    };
    Sub::Genius->new(pre => $pre)->run_any();

STATIC CODE UTILITY METHODS

The stubby utility is provided for this purpose and is not part of the main module.

SEE ALSO

Pipeworks, Sub::Pipeline, Process::Pipeline, FLAT, Graph::PetriNet

GOOD READINGS

COPYRIGHT AND LICENSE

Same terms as perl itself.

AUTHOR

OODLER 577 <oodler@cpan.org<gt>

ACKNOWLEDGEMENTS

TEODESIAN is acknowledged for his support and interest in this project, in particular his work lifting the veil off of what passes for concurrency these days; namely, most of the "Async" modules out there are actually fakin' the funk with coroutines.. See https://troglodyne.net/video/1615853053 for a fun, fresh, and informative video on the subject.