IO::Lambda: One Wait At A Time (Part I)

On CPAN there are few options for reactive, event-driven programming, POE being just the most prominent one.

One hidden gem in this category is IO::Lambda. Given that it uses strongly a functional paradigm and continuations it will probably alienate most procedurally wired brains. The documentation and the examples are rich, but that itself presents a high entrance barrier. Which is a shame, because IO::Lambda is quite brilliant.

And it is minimalistic, as all is based on the concept of a lambda. That is a piece of code which first initializes some data structure, one which is relevant for one particular state. Then the lambda sets up conditions based on that data structure. When eventually one such condition is satisfied, a callback code is executed. That may again initialize something, and again wait for more conditions to become true. And so forth until there is nothing left to wait for.

A Gentle Introduction

There several confusing things to digest, though. If you create your lambda as

use IO::Lambda qw(:all);

my $q = lambda {
    context 5;
    warn "I will wait for you now.";
    timeout {
       warn "I waited for 5 seconds";
    }
};

Then it is just passively sitting there. To make it active you have to wait for its completion (at least that's one option):

$q->wait;

Only then the lambda will start the code block.

The Context

That block itself may do some work before it may decide to wait for an asynchronous event to happen. In the case above it is simply a timeout of 5 seconds, but more realistically some communication will be involved. We look at that later.

For the waiting part there is some magic: If you put an integer into the context and then use the condition timeout, it will wait for that many seconds.

If you otherwise put a time onto the stack (especially with Time::HiRes), and use timeout the lambda will use the float as deadline:

use Time::HiRes qw(time);
my $deadline = time + 5;

my $q = lambda {
    context $deadline;
    timeout {
       warn "I again waited for 5 seconds";
    }
};

Note how the $deadline variable is in the closure.

The Flow (Vertical)

The fact that the lambda is only setting up contexts and then waits for one of the conditions to happen has some unusual consequences. Look at the following:

my $q = lambda {
    context 5;
    timeout { warn "I waited for 5 seconds"; };
    context 10;
    timeout { warn "I waited for 10 seconds"; };
};

What will not happen is that the lambda waits for 5 and then after that for more 10 seconds. No, it will wait for both timeouts at the same time:

mando:tmp rho$ perl test.pl
.... time passes .... after 5 secs
I waited for 5 seconds at test.pl line 5.
.... time passes .... after 5 secs
I waited for 10 seconds at test.pl line 7.

So don't read this procedurally. Unless it is normal Perl code, of course.

Here is a little test for you:

my $q = lambda {
    context 5;
    timeout { warn "I waited a while"; };
    timeout { warn "I also waited a bit"; };
};

After which times do you see the outputs?

The Flow (Horizontal)

Once a condition is satisfied and the appropriate code segment is executed that may also decide to wait for yet another event:

my $q = lambda {
    context 5;
    timeout {
        warn "I waited for 5 seconds";
        context 5;
        timeout {
           warn "I waited for another 5 seconds";
        }
    }};

After the first timeout, the callback code sets up another 5 second timeout. After that has written the second output the lambda is exhausted as it does not wait for any other event. As always in that case, it will terminate.

Obviously this nesting of events can go to any level of depth. But if data is involved, then it should be allocated at the proper syntactic level. In the following we first - for no particular reason other than to show some nesting - wait for 5 seconds:

my $q = lambda {
    context 5;
    timeout {
       warn "this after 5 secs";
       my $count = 0;
       context 1;
       timeout {
           warn "tick ...";
           again if ++$count < 3;
       }
    }};

Inside the timeout block we allocate a counter which is global to the next inner timeout. In there we repeat the current thing we waited for (aka again) but only if the counter is less than 3. And that only works if the counter is shared among the timeout invocations. Hence it is allocated one level up.

Looking Back

You can see now why many programmers will not be easily able to wrap their minds around IO::Lambda. It uses Perl's syntactic structure in a declarative, yet non-intuitive way and on top of it it uses closures to model the flow of control.

While unorthodox, this approach allows a high degree of flexibility as you can dynamically generate conditions and/or callback blocks. That is difficult to achieve elegantly with POE.

Also interesting is the possibility to nest lambdas within each other. Or hold them on stock, only to reuse them later. I will look at that in some other installment.

Posted In