[The following is a large collection of notes and ramblings I've been
thinking on... any comments/thoughts would be appreciated]

So... Hook lists. They're a source of events.

They're named. Events have arguments, and perhaps return values. Simple events 
ignore their returns. More complex ones might do fancier things. More on this 
later.

Event arguments are passed by-reference, so handlers get the chance to mangle 
them, and the modifications visible to later handlers.

Modules can hook into these and insert their own handlers. Each event might 
have lots of handlers. So ordering becomes a problem. More on this later too.

Handler functions might be synchronous, might be async. They might want to run 
forever, they might want to be removed at some time. Maybe even we'll want to 
remove one ahead-of-time.

So what's our API look like? Does the "hook list" exist as a firstclass object? 
It might want to... Or an object that has hooklists might be in a role, that 
supports them by names.

So our basic registration API might look like one of the following:

  $hooklist->register_sync( \&my_handler );
  $hooklist->register_async( \&my_async_handler );

  $object->register_hook_sync( "event", \&my_handler );
  $object->register_hook_async( "event", \&my_async_handler );

With handlers that looked like:

  sub my_handler
  {
     my ( $event, @args ) = @_;
     print "$event happened with @args\n";
     return;
  }

  sub my_async_handler
  {
     my %params;
     my $event = $params{event};
     my @args  = @{ $params{args} };

     print "$event happened with @args\n";

     # Typically this would be called sometime later; it invokes a 'next'
     # continuation that was passed in
     $params{next}->();  
  }

We can call them something like:

  $hooklist->fire( @args );     # Would only work if there's no async. ones
  $hooklist->fire_async( args => [EMAIL PROTECTED], on_done => sub { .... } );

  $object->fire_hook( "event", @args ); # Ditto
  $object->fire_hook_async( event => "event", args => [EMAIL PROTECTED], 
on_done => sub { .... } );

So what about hooks to be removed? Synchronous ones can just return some value 
to say if they want removing or not:

   sub my_first_handler
   {
      ...
      return $hang_around ? 1 : 0;
   }

What about async. ones? They might not know yet by the time the function 
returns if they want to persist or not. So maybe we give them an ID and some 
way to remove themself?

  sub my_second_handler
  {
     my %params = @_;
     my $id = $params{id};
     my $hooklist = $params{hooklist};

     ...

     later( sub {
        $hooklist->unregister_hook( $id ) if not $hangaround;
        $params{next}->();
     } );
  }

At which point, we have enough of an API to actually consider making the 
synchronous ones do that too, and have the registration functions return the ID

  my $id = $hooklist->register_sync( \&my_handler );

  $hooklist->unregister( $id ) if $cancel;

  sub my_handler
  {
     my %params = @_;    # Make the API look the same as for sync. ones

     my $id       = $params{id};
     my $hooklist = $params{hooklist};

     # do stuff synchronously
     $hooklist->remove( $id ) if $allfinished;

     return;
  }

So now we don't have any significance on the return value any more. We'll need 
this later - see below.

Now we might consider some more interesting examples. Suppose now we actually 
care about ordering. And also we allow that any plugin might decide to halt the 
rest of the list; whatever the event was, it's been handled and there's no need 
for anyone else to look at it.

So what about ordering? Maybe we give a simple priority number at reg. time?

   $object->register_hook_sync( "event", 10, \&my_first_handler );
   $object->register_hook_async( "event", 20, \&my_second_handler );

This works if we can do it; if we can all agree upfront what we mean by 10, 20, 
100, whatever. But this might get hard to coordinate in the 
necessarily-distrubted environment of whatever it was that needed these 
pluggable hooklists in the first place.

Stopping the list early becomes easy enough though; either synchronously or 
async:

  sub my_sync_handler
  {
     my %params = @_;

     .....

     return $handled ? 1 : 0;
  }

  sub my_async_handler
  {
     my %params = @_;

     .....

     later( sub {
        # Choose which continuation
        $params{ $handled ? "last" : "next" }->();
     } );
  }

The result of the call can then say what happened:

  my $handled = $hooklist->fire( @args );
  $hooklist->fire_async( args => [EMAIL PROTECTED], on_done => sub {
     my ( $handled ) = @_;
  } );

Of course, now we have the simplest of hooklist aggregations - the "repeat 
while false" one. What if we wanted something else? Suppose we wanted to sum 
some quantity?

  my $hooklist = Hooklist->new(
     aggregate => sub { $a + $b }, # accumulate in $a using the per-hook value 
$b
  );

  $hooklist->register_sync( \&my_handler );
  $hooklist->register_async( \&my_async_handler );

  $hooklist->fire_async( 
     initial => 0,

     on_done => sub {
        my ( $total ) = @_;
        print "Total: $total\n";
     }
  );

  sub my_handler
  {
     return 20;
  }

  sub my_async_handler
  {
     my %params = @_;

     later( sub {
        $params{next}->( $count );
     } );
  }

If we now give the aggregation function the ability to control the loop, we 
find that the next/last behaviour above falls out as a natural consequence of 
how it works:

  my $hooklist = Hooklist->new(
     aggregate => sub { last HOOK if $b },
  );

One Big Question still remains. Ordering. Do we just take priority numbers, or 
can we find something better?

Perhaps anyone who cares about ordering has a name, and others can ask to be 
somewhere before/after those named points?

  $hooklist->register_sync(
     name    => "mangle",
     handler => \&my_mangle_function,
  );

  $hooklist->register_sync(
     name    => "eat",
     handler => \&my_eat_function,
     after   => [ "mangle" ],
  );

Then sometime later, some wants to look at this event after it's been mangled 
but before it's been eaten:

  $hooklist->register_sync(
     name    => "observe",
     handler => \&my_observe_function,
     after   => [ "mangle" ],
     before  => [ "eat" ],
  );

This has the ability to throw an exception if the ordering conditions cannot be 
met.

This solution has now changed the problem from one of managing absolute 
numbers, into one of managing names. Hopefully less chance of collisions and 
mistaken behaviour, but it's more complex to implement and handle. Also harder 
for the "end user" to picture the behaviour. But maybe it's worth it?

Another interesting question - if two async. handlers are installed at the same 
priority, would it be best for us to take advantage of that, and run them both 
in parallel? If we do, we can maximise IO performance of whatever they do, but 
we run the interesting risk that one of them asks to stop; we can't stop the 
other one. But maybe that's "hard luck", and just what you get from taking the 
same number twice..?

-- 
Paul "LeoNerd" Evans

[EMAIL PROTECTED]
ICQ# 4135350       |  Registered Linux# 179460
http://www.leonerd.org.uk/

Attachment: signature.asc
Description: PGP signature

Reply via email to