Comparing react with tap
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.