[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/
signature.asc
Description: PGP signature
