Sterling has too many projects Blogging about programming, microcontrollers & electronics, 3D printing, and whatever else...

Comparing react with tap

| 879 words | 5 minutes | raku advent-2019
A water spout with flowers

In Raku we have a couple basic ways of getting at the events emitted from a Supply, which begs the question, what’s the difference between each? I want to answer that question by creating a react block with a couple intervals and then emulate the same basic functionality using tap.

Let’s start with our base react block:

sub seconds { state $base = now; now - $base }
react {
    say "REACT 1: {seconds}";

    whenever Supply.interval(1) {
        say "INTERVAL 1-$_: {seconds}";
        done if $_ > 3;
    }

    say "REACT 2: {seconds}";

    whenever Supply.interval(0.5) {
        say "INTERVAL 2-$_: {seconds}";
    }

    say "REACT 3: {seconds}";
}

The seconds routine is just a helper to give us time in seconds from the start of the block to work from. The output from this block will typically be similar to this:

REACT 1: 0.0011569
REACT 2: 0.0068571
REACT 3: 0.008015
INTERVAL 1-0: 0.0092906
INTERVAL 2-0: 0.0101116
INTERVAL 2-1: 0.5103139
INTERVAL 1-1: 1.007995
INTERVAL 2-2: 1.022309
INTERVAL 2-3: 1.5124228
INTERVAL 1-2: 2.0137509
INTERVAL 2-4: 2.014717
INTERVAL 2-5: 2.517795
INTERVAL 1-3: 3.016291
INTERVAL 2-6: 3.0182612
INTERVAL 2-7: 3.521018
INTERVAL 1-4: 4.0182113

So what’s it mean? Well, first thing to note is that all the code in the react block itself runs first. That is, it runs all the commands, including each whenever block to register the event taps for each Supply, but not to run the code yet. Once the react block finishes running, it blocks until either all the whenever blocks are done or the done statement is encountered. At that point, all the supplies are untapped and execution continues.

By the way, if you want to have a block run after a react block has completed (or a supply block for that matter), you can use the special CLOSE phaser. A LEAVE phaser will exit immediately when the code in the react block finishes setting up the react.

Aside from that, it must be noted that everything related to the react block will only run in sequence. Raku doesn’t promise to run it in a single thread, but it does promise that no two parts of the code inside of a react block will run concurrently. This includes the first run through executing the react block itself as well as executing the whenever blocks in reaction to emitted values to supplies.

So, how would we go about this behavior using .tap? We could do it like this:

sub seconds { state $base = now; now - $base }
REACT: {
    say "REACT 1: {seconds}";

    my $ready = Promise.new;
    my $mutex = Lock.new;
    my $finished = my $done = Promise.new;

    my $interval1 = Supply.interval(1).tap: {
        await $ready;
        $mutex.protect: {
            say "INTERVAL 1-$_: {seconds}";
            $done.keep if $_ > 3;
        }
    }

    $finished .= then: -> $p {
        $interval1.close;
    }

    say "REACT 2: {seconds}";

    my $interval2 = Supply.interval(0.5).tap: {
        await $ready;
        $mutex.protect: {
            say "INTERVAL 2-$_: {seconds}";
        }
    }

    $finished .= then: -> $p {
        $interval2.close;
    }

    say "REACT 3: {seconds}";

    $ready.keep;
    await $finished;
}

This is similar to what the react block is actually doing, but with several additional manual steps. First we must prepare a couple of promises. The $ready Promise is kept at the end of the “REACT” block to release the taps to do their work. The $done Promise is where we hold the main thread until execution is complete.

I have not implemented the additional logic of automatically keeping $done if all supplies become done. Doing so could be done by creating another Promise for each tap that is kept when the tap done block is executed. A .then block could be attached to a Promise.allof() promise for all those promises. I leave solving that as an exercise for the reader.

The other major addition is the $mutex Lock object. This prevents the individual tap blocks from running simultaneously.

That should be enough. This is probably not the most efficient solution, but it does demonstrate the extra help the react block gives you. You may notice that the tap version is ever so slightly faster. This should not be a surprise. This tap version is not taking as much care and organization as the react block. Therefore, if eeking out a few extra milliseconds matters to your code, you may want to consider implementing your async coordination code directly using tap and some other tools rather than a react block. However, be aware that the react block is likely saving you a pile of headaches in debugging by doing all those fiddly little details for you.

And one final note, the documentation of the act method states that it works like tap, but the given code is executed by only one thread at a time. I’m really uncertain as to what this really means because this same basic guarantee is inherent to tap as well. This is because a Supply is unable to continue with another emitted message until all taps have finished running. In practice, taps all run synchronously for each message too. I haven’t found any evidence in all my work that taps on a given supply ever run concurrently. Anyway, if someone can go on to the Reddit thread for this post and explain what the actual difference is between tap and act, I would appreciate it.

Cheers.

The content of this site is licensed under Attribution 4.0 International (CC BY 4.0).