NAME
Async::Defer - VM to write and run async code in usual sync-like way
SYNOPSIS
use Async::Defer;
# ... CREATE
my $defer = Async::Defer->new();
my $defer2 = $defer->clone();
# ... SETUP
$defer->do(sub{
my ($d, @param) = @_;
# run sync/async code which MUST end with one of:
# $d->done(@result);
# $d->throw($error);
# $d->continue();
# $d->break();
});
$defer->if(sub{ my $d=shift; return 1 });
$defer->try();
$defer->do($defer2);
$defer->catch(
qr/^io:/ => sub{
my ($d,$err) = @_;
# end with $d->done/throw/continue/break
},
qr// => sub{ # WILL CATCH ALL EXCEPTIONS
my ($d,$err) = @_;
# end with $d->done/throw/continue/break
},
FINALLY => sub{
my ($d,$err,@result) = @_;
# end with $d->done/throw/continue/break
},
);
$defer->else();
$defer->while(sub{ my $d=shift; return $d->iter() <= 3 });
$defer->do(sub{
my ($d) = @_;
# may access $d->iter() here
# end with $d->done/throw/continue/break
});
$defer->end_while();
$defer->end_if();
$defer->{anyvar} = 'anyval';
# ... START
$defer->run();
DESCRIPTION
WARNING: This is experimental code, public interface may change.
This module's goal is to simplify writing complex async event-based code, which usually mean huge amount of callback/errback functions, very hard to support. It was initially inspired by Python/Twisted's Deferred object, but go further and provide virtual machine which allow you to write/define complete async program (which consists of many callback/errback) in sync way, just like you write usual non-async programs.
Main idea is simple. For example, if you've this non-async code:
$var = fetch_val();
process_val( $var );
and want to make fetch_val()
async, you usually do something like this:
fetch_val( cb => \&value_fetched );
sub value_fetched {
my ($var) = @_;
process_val( $var );
}
With Async::Defer you will split initial non-async code in sync parts (usually this mean - split on assignment operator):
### 1
fetch_val();
### 2
$var =
process_val( $var );
then wrap each part in separate anon sub and add Defer object to join these parts together:
$d = Async::Defer->new();
$d->do(sub{
my ($d) = @_;
fetch_val( $d ); # will call $d->done('…result…') when done
});
$d->do(sub{
my ($d, $var) = @_;
process_val( $var );
$d->done(); # this sub is sync, it call done() immediately
});
$d->run();
These anon subs are similar to statements in perl. Between these statements you can use flow control operators like if()
, while()
and try()
/catch()
. And inside statements you can control execution flow using done()
, throw()
, continue()
and break()
operators when current async function will finish and will be ready to go to the continue step. Finally, you can use Async::Defer object to keep your local variables - this object is empty hash, and you can create any keys in it. Single Defer object described this way is sort of single function. And it's possible to call another functions by using another Defer object as parameter for do()
instead of usual anon sub.
While you can use both sync and async sub in do()
, they all MUST call one of done()
, throw()
, continue()
or break()
when they finish their work, and do this ONLY ONCE. This is Defer's way to proceed from one step to another, and if not done right Defer object's behaviour is undefined!
PERSISTENT STATE, LOCAL VARIABLES and SCOPE
There are several ways to implement this, and it's unclear yet which way is the best. We can implement full-featured stack with local variables similar to perl's local
using getter/setter methods; we can fill called Defer objects with copy of all keys in parent Defer object (so called object will have full read-only access to parent's scalar data, and read/write access to parent's reference data types); we can do nothing and let user manually send all needed data to called Defer object as params and get data back using returned values (by done()
or throw()
).
In current implementation we do nothing, so here is some ways to go:
### @results = another_defer(@params)
$d->do(sub{
my ($d) = @_;
my @params_for_another_defer = (…);
$d->done(@params_for_another_defer);
});
$d->do($another_defer);
$d->do(sub{
my ($d, @results_from_another_defer) = @_;
...
$d->done();
});
### share some local variables with $another_defer
$d->do(sub{
my ($d) = @_;
$d->{readonly} = $scalar;
$d->{readwrite} = $ref_to_something;
$another_defer->{readonly} = $d->{readonly};
$another_defer->{readwrite} = $d->{readwrite};
$d->done();
});
$d->do($another_defer);
$d->do(sub{
my ($d) = @_;
# $d->{readwrite} here may be modifed by $another_defer
$d->done();
});
### share all variables with $another_defer (run it manually)
$d->do(sub{
my ($d) = @_;
%$another_defer = %$d;
$another_defer->run($d);
});
$d->do(sub{
my ($d) = @_;
# all reference-type keys in $d may be modifed by $another_defer
$d->done();
});
If you want to reuse same Defer object several times, then you should keep in mind: keys created inside this object on first run won't be automatically removed, so on second and continue runs it will see internal data left by previous runs. This may or may not be desirable behaviour. In later case you should use clone()
and run only clones of original object (clones are created using %$clone=%$orig
, so they share only reference-type keys which exists in original Defer):
$d->do( $another_defer->clone() );
$d->do( $another_defer->clone() );
EXPORTS
Nothing.
INTERFACE
- new()
-
Create and return Async::Defer object.
- clone()
-
Clone existing Async::Defer object and return clone.
Clone will have same program (STATEMENTS and OPERATORS added to original object) and same local variables (non-deep copy of orig object keys using
%$clone=%$orig
). After cloning these two objects can be modified (by adding new STATEMENTS, OPERATORS and modifying variables) independently.It's possible to
clone()
object which is running right now, cloned object will not be in running state - this is safe way torun()
objects which may or may not be already running. - run( [ $parent_defer, @params ] )
-
Start executing object's current program, which must be defined first by adding at least one STATEMENT (
do()
or<catch(FINALLY=
sub{})>>) to this object.Usually while
run()
only first STATEMENT will be executed (with optional@params
in parameters). It will just start some async function and returns, andrun()
will returns immediately after this too. Actual execution of this object will continue when started async function will finish (usually after Timer or I/O event) and call this object'sdone()
,break()
,continue()
orthrow()
methods.It's possible to make all STATEMENTS sync - in this case full program will be executed before returning from
run()
- but this has no real sense because you don't need Defer object for sync programs.If
run()
used to start top-level program (i.e. without$parent_defer
parameter), then there will be no return value at end of program - after break STATEMENT in this object will calldone()
nothing else will happens and any parameters of that breakdone()
call will be ignored. If this Defer object was started as part of another program (i.e. it was added there usingdo()
or just manually executed from some STATEMENT with defined$parent_defer
parameter), then it return value will be delivered to continue STATEMENT in$parent_defer
object. - iter()
-
This method available only inside
while()
- both inwhile()
's\&conditional
argument andwhile()
's body STATEMENTS. It return current iteration number for nearestwhile()
, starting from 1.# this loop will execute 3 times: $d->while(sub{ shift->iter() <= 3 }); $d->do(sub{ my ($d) = @_; printf "Iteration %d\n", $d->iter(); $d->done(); }); $d->end_while();
STATEMENTS and OPERATORS
- do( \&sync_or_async_code )
- do( $child_defer )
-
Add STATEMENT to this object's program.
When this STATEMENT should be executed,
\&sync_or_async_code
(or$child_defer
's first STATEMENT) will be called with these params:( $defer_object, @optional_results_from_previous_STATEMENT )
- do( [\&sync_or_async_code, $child_defer, …] )
- do( {task1=>\&sync_or_async_code, task2=>$child_defer, …} )
-
Add one STATEMENT to this object's program.
When this STATEMENT should be executed, all these tasks will be started simultaneously (Defer objects using
clone()
andrun()
, code by transforming into new Defer object and then alsorun()
). This program will continue only after all these tasks will be finished (either withdone()
orthrow()
).It's possible to provide params individually for each of these tasks and receive results/error returned by each of these tasks, but actual syntax depends on how these tasks was named - by id (ARRAY) or by name (HASH):
$d->do(sub{ my ($d) = @_; $d->done( ['param1 for task1', 'param2 for task1'], ['param1 for task2'], [undef, 'param2 for task3'], # no params for task4,task5,… ); }); $d->do([ $d_task1, $d_task2, $d_task3, $d_some, $d_some ]); $d->do(sub{ my ($d, @taskresults) = @_; my $id = 1; if (ref $taskresults[$id-1]) { print "task $id results:", @{ $taskresults[$id-1] }; } else { print "task $id throw error:", $taskresults[$id-1]; } }); $d->do(sub{ my ($d) = @_; $d->done( task1 => ['param1 for task1', 'param2 for task1'], task2 => ['param1 for task2'], task3 => [undef, 'param2 for task3'], # no params for task4,task5,… ); }); $d->do({ task1 => $d_task1, task2 => $d_task2, task3 => $d_task3, task4 => $d_some, task5 => $d_some, }); $d->do(sub{ my ($d, %taskresults) = @_; if (ref $taskresults{task1}) { print "task1 results:", @{ $taskresults{task1} }; } else { print "task1 throw error:", $taskresults{task1}; } });
- if( \&conditional )
- else()
- end_if()
-
Add conditional OPERATOR to this object's program.
When this OPERATOR should be executed,
\&conditional
will be called with single param:( $defer_object )
The
\&conditional
MUST be sync, and return true/false. - while( \&conditional )
- end_while()
-
Add loop OPERATOR to this object's program.
When this OPERATOR should be executed,
\&conditional
will be called with single param:( $defer_object )
The
\&conditional
MUST be sync, and return true/false. - try()
- catch( $regex_or_FINALLY => \&sync_or_async_code, ... )
-
Add exception handling to this object's program.
In general, try/catch/finally behaviour is same as in Java (and probably many other languages).
If some STATEMENTS inside try/catch block will
throw()
, the thrown error can be intercepted (using matching regexp incatch()
) and handled in any way (blocked - ifcatch()
handler calldone()
,continue()
orbreak()
or replaced by another exception - ifcatch()
handler callthrow()
). If exception match more than one regexp, first successfully matched regexp's handler will be used. Handler will be executed with params:( $defer_object, $error )
In addition to exception handlers you can also define FINALLY handler (by using string
"FINALLY"
instead of regex). FINALLY handler will be called in any case (with/without exception) and may handle this in any way just like any other exception handler incatch()
. FINALLY handler will be executed with different params:# with exception ( $defer_object, $error) # without exception ( $defer_object, @optional_results_from_previous_STATEMENT )
FLOW CONTROL in STATEMENTS
One, and only one of these methods MUST be called at end of each STATEMENT, both sync and async!
- done( @optional_result )
-
Go to continue STATEMENT/OPERATOR. If continue is STATEMENT, it will receive
@optional_result
in it parameters. - throw( $error )
-
Throw exception. Nearest matching
catch()
or FINALLY STATEMENT will be executed and receive$error
in it parameter. - continue()
-
Move to beginning of nearest
while()
(or to first STATEMENT if called outsidewhile()
) and continue with continue iteration (ifwhile()
's\&conditional
still returns true). - break()
-
Move to first STATEMENT/OPERATOR after nearest
while()
(or finish this program if called outsidewhile()
- returning to parent's Defer object if any).
BUGS AND LIMITATIONS
No bugs have been reported.
SUPPORT
Please report any bugs or feature requests through the web interface at http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Async-Defer. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
You can also look for information at:
RT: CPAN's request tracker
AnnoCPAN: Annotated CPAN documentation
CPAN Ratings
Search CPAN
AUTHOR
Alex Efros <powerman-asdf@ya.ru>
LICENSE AND COPYRIGHT
Copyright 2011 Alex Efros <powerman-asdf@ya.ru>.
This program is distributed under the MIT (X11) License: http://www.opensource.org/licenses/mit-license.php
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.