In the previous post I introduced the idea of Event-Reflexive programming, and discussed the first use-case I had for it; driving the user provisioning system at an ISP. I said this story would continue in chronological order.
A couple of years into this job, I felt I had learned Perl enough to do what surely pretty-much any Perl developer does at this time. Yes, I decided to write an IRC bot. It's one of those rites of passage that every developer goes through at some point. Of course, even this early in my programming career I had already seen several dozen terrible attempts at this, so I was quite determined to ensure mine wouldn't suffer quite as many of those mistakes. In my head, of course, I knew I wouldn't suffer many mistakes because I, of course, was armed with Event-Reflexive Programming.
I ended up with, I thought, the most amazing (it wasn't), the most powerful (it wasn't) and the most flexible (it wasn't) IRC bot the world had ever seen (it isn't). However, in the process of building it I had expanded on the original concept of event-reflexivity considerably.
In the previous post I introduced the most basic two forms of invoking the plugins in an event-reflexive system, run_plugins and run_plugins_reverse. In the course of developing this IRC bot, I found it necessary to create a number of other variations on this basic theme.
Recalling the original two functions, both of these simply execute action hooks defined by the plugins. Neither of them returns an interesting result. What I found while implementing the IRC bot was that as well as merely requesting that work be performed, I was also using the event-reflexive core to abstract out a number of query-like operations - such as abstracting away the specific mechanism of database used to store information about registered users. At this point, the event-reflexive core needs a way to pose a question to the list of plugins, and return an answer as soon as one has been provided:
sub ask_plugins { my ( $query, @args ) = @_ foreach my $plugin ( @plugins ) { next unless $plugin->can( $query ); my $ret = $plugin->$query( @args ); return $ret if defined $answer; } return undef; }
Perl being Perl, it's only a short matter of time before we want a list-valued return from some of these queries. And once we're returning a list, we're not restricted to returning the result from a single plugin - we can run them all:
sub ask_plugins_list { my ( $query, @args ) = @_; my @ret; foreach my $plugin ( @plugins ) { next unless $plugin->can( $query ); push @ret, $plugin->$query( @args ); } return @ret; }
The final and most interesting invocation function was called scatter_plugins. This being written years before I had encountered the concept of Futures, it was initially written with a complex combination of additional code reference arguments, before I managed to neaten it up somewhat with the creation of Async::MergePoint. I won't give the implementation here, but the point of this particular call was to account for the fact that some plugin actions are going to be asynchronous, and only return a result later. What we'd like to do is start all the operations concurrently, then await their eventual completion before continuing.
These days, I would instead implement this operation using Future. In fact, at this point a case could be made for implementing all of them using Future. If the entire event-reflexive core was based on futures, then trivially it will cope with any synchronous or asynchronous kind of work environment (due to the universal suitability of futures). If we currently set aside our previous question of plugin ordering, and assert that ordering doesn't matter, then all the remaining operations besides reverse can be expressed on top of a single idea:
sub _call_all_plugins { my ( $method, @args ) = @_; return map { my $plugin = $_; $plugin->$method( @args ); } grep { $_->can( $method ) } @plugins; } sub run_plugins_concurrently { Future->needs_all( _call_all_plugins( @_ ) ) ->then_done( "" ); # return an empty result } sub ask_plugins_concurrently { Future->needs_any( _call_all_plugins( @_ ) ) } sub ask_all_plugins { Future->needs_all( _call_all_plugins( @_ ) ) }
In fact at this point our previous idea of scatter_plugins becomes totally redundant - the universal expressiveness of Futures has allowed this to be expressed even simpler. But this incredibly simple implementation has come at a cost - we've lost the sequential lazy-evaluation nature of ask_plugins. Additionally, whatever stop-on-error semantics we might have wanted out of run_plugins have been lost.
Perhaps instead we decide we need ordering, at least in some cases. This brings to mind some additional invocation functions, that themselves are also wrappers around a single common idea:
use Future::Utils qw( repeat ); sub _call_each_plugin { my ( $reverse, $while, $method, @args ) = @_; repeat { my ( $plugin ) = @_; $plugin->$method( @args ); } foreach => [ grep { $_->can( $method ) } $reverse ? reverse(@plugins) : @plugins ], while => $while; } sub run_plugins_sequentially { _call_each_plugin( 0, sub { 1 }, @_ ); } sub run_plugins_sequentially_reverse { _call_each_plugin( 1, sub { 1 }, @_ ); } sub ask_plugins_sequentially { _call_each_plugin( 0, sub { not shift->get }, @_ ); }
Keeping in mind my first question from the previous post, on the subject of ordering between plugins, this motivates a choice of second questions:
If ordering guarantees are not required, are the concurrent invocation functions given above sufficient to express any order-less possibly-asynchronous operation in an event-reflexive system?
If ordering is required, are the additional sequential invocation functions sufficient to express any ordered possibly-asynchronous operation?
No comments:
Post a Comment