When we talk about measuring time, we could be thinking of a number of different ways to measure a number of different things. But in principle, I suppose we could group them into two large categories:
-
We could be measuring the time that has passed in relation to some previous event, to see for example how much time has passed since (like how long it takes for you to count to 100); or
-
We could be measuring the time up to some future event, to see for example if some thing has happened before that (like if it's time to take the cake out of the oven).
These scenarios being common enough, it's no surprise Raku has them well covered:
We can, for example, measure how long something took like this:
my $start = now;
my $prime = (^∞).grep(*.is-prime)[999]; # Do something slow...
say "Took { now - $start } seconds to find the 1000th prime: $prime";
# OUTPUT:
# Took 0.9206044 seconds to find the 1000th prime: 7919
We can do this because Raku has classes to help us get this right. The now
routine returns an Instant, which represents a specific moment in time. And
when we perform arithmetic on these objects (like when we did now - $start
)
we get a Duration object, which represents a length of time.
Many of the common time operations are well supported by these time classes (and other related ones), and their documentation is a good place to look for ideas on what they are capable of, and how they can be used.
To "count down" to a future event, we need to be able to represent one, and Raku has this too. It's called a Promise, and it represents the result of a process that has not yet ended. In a sense, a Promise is a placeholder value, a promise from the runtime that, when some pending process has finished, we'll be able to find its result.
This may sound abstract, and this is because... it is! Unlike say an Instant or a Duration, which represent specific aspects of time, a Promise could represent whatever the result of that process is, for whatever process. This result could be a moment in time, but it could also be the text of a web page, or the outcome of some complicated mathematical operation, or anything, really.
To illustrate, here's how we could use one of these Promise objects to measure the number of primes we can find before time runs out:
my $one-second-passed = Promise.in: 1;
my @primes = gather for (^∞).grep: *.is-prime {
.take;
last if $one-second-passed;
}
await $one-second;
say "Found { @primes.elems } primes in 1 second";
# OUTPUT:
# Found 1057 primes in 1 second
We create a Promise that will be kept in one second to serve as our timeout,
and we populate our list of primes inside a loop using gather
and take
.
After adding a new prime to our list, we check if the Promise has been kept,
and if so, we stop.
We can cover many simple cases with what we've mentioned so far, and a lot of the time solutions like these will likely be enough. But there are cases when these basic tools become insufficient.
Let's say that, instead of measuring how long it takes for you to count to 100, the task is now to count how many times you can count to 100 in under a set amount of time.
Or if you'd like a more realistic example, think for instance of a connection to a server that sends a heartbeat every 30 seconds. If we've received no heartbeat after that time, we want to close the connection.
Or maybe a process that needs to batch requests to an external service. We want to wait up to a second after each request has been generated before firing off a batch and continue to wait for the next one.
These scenarios are a combination of the two cases we discussed above:
-
They measure from a point in the past: the point at which you started a new count, or received a heartbeat, or generated a request
-
They measure towards a point in the future: the point at which you'll run out time to finish your count, or when we've decided no heartbeat is coming, or that it is time to send a new request batch
The key difference here is that the "deadline" in these cases is not a fixed one: if we do receive a new heartbeat under the time limit, the countdown is reset, and we start counting from scratch.
As it turns out, the current design of a Promise makes this a bit awkward to represent, because they represent the placeholder for the result of a pending process. This means they have no direct control over that process. And this is the way it should be if we want them to be applicable to the largest number of scenarios.
Consider this naïve version of the code above:
my @primes;
await Promise.anyof(
Promise.in(1),
start { # See below for why this is a bad idea
@primes.push: $_ for (^∞).grep: *.is-prime;
},
);
say "Found { @primes.elems } primes in 1 second";
# OUTPUT:
# Found 1057 primes in 1 second
This bit of code will generate the same output as the one above, but it does
not behave the same. The key difference (and problem) is that this code
will never stop pushing elements to @primes
.1 This is because the
process that is started by the start
keyword will continue to run for as
long as it can, even if we are no longer paying attention.
Luckily, Raku is a language that is made to be extended, and it just so happens that a solution for this problem exists in the form of Timer::Breakable.
Timer::Breakable can be understood as a type of Promise that, like pie crust, is made to be broken.2 And with it, we can solve the problem of this moving goalpost:
use Timer::Breakable;
my $disconnect = Promise.new;
my $heartbeat = Supply.interval(0.5).grep: { Bool.pick }
my Timer::Breakable $timeout;
react {
whenever $disconnect { done }
whenever $heartbeat {
say '-- THUMP --';
.stop with $timeout;
$timeout = Timer::Breakable.start: 0.75, {
say 'No heartbeat! Disconnecting...';
$disconnect.keep;
}
}
}
# OUTPUT:
# -- THUMP --
# -- THUMP --
# -- THUMP --
# -- THUMP --
# -- THUMP --
# No heartbeat! Disconnecting...
We generate an irregular stream of heartbeats using a Supply (we'll come
back to this later, I promise) and listen to them within a react
block.
We also create a new Promise that will represent the connection: once it
is kept, we can assume it's safe to disconnect.
Every time we receive a new heartbeat, the .stop with $timeout
checks
whether we've already defined a timer and if so stops is (if we didn't
we'd interrupt the connection even if a new heartbeat had arrived on time).
We then create a new timer that will wait for a set amount of time (0.75
seconds in this case) before keeping our Promise. Since we are waiting
for that Promise (with the first whenever
), that will allow us to
detect when we can close the "connection".
There is one more scenario that we mentioned above as an example, and that we haven't quite covered: the batching case.
Fundamentally, this is not different from the one we just looked at, with the difference that instead of completely disconnecting, what we want to do when the time runs out is to process the batch and continue to wait, this time for the next batch of items.
And this brings us to another of the limitations of a Promise: they represent the outcome of one pending process. Or, in other words, they can be kept or broken, but only once.
Like with the limitation we mentioned above, there is good reason for this to
be the case: since Raku allows us to work with highly asynchronous code,
having a Promise only change from Planned
to some other state once
limits a whole series of problems that we don't need to worry about.3
However, in this particular case this creates a problem for us. And indeed one
that we have already had to work around in the snippet above: every time we
received a new heartbeat we had to create a new $timer
. We cannot reset it.
Since Timer::Breakable wraps around a Promise, it's no surprise it inherits this feature.
So in this case a Promise is not the right tool for the job. Instead, we can finally keep our promise made above, and reach for a Supply.
A Supply is an asynchronous stream of data that can be used by multiple observers in our program. We used one already to represent our irregular series of heartbeats, and this time we'll add one to represent the stream of batches ready to be processed:
use Timer::Breakable;
my $batcher = Supplier.new;
my $batch = $batcher.Supply;
my $stream = Supply.interval(0.5).grep: { Bool.pick }
my Timer::Breakable $timeout;
my @batch;
react {
whenever $batch {
say "Received a batch: { @batch.join: ' ' }";
@batch = ();
}
whenever $stream {
say "Queuing $_";
@batch.push: $_;
.stop with $timeout;
$timeout = Timer::Breakable.start: 0.75, {
$batcher.emit: True
}
}
}
# OUTPUT:
# Queuing 0
# Received a batch: 0
# Queuing 2
# Received a batch: 2
# Queuing 8
# Queuing 9
# Queuing 10
# Received a batch: 8 9 10
# ...
The code here is similar to the previous example, with the difference that
instead of keeping a Promise to disconnect, when the timer runs out and
we are ready to process the batch, we emit
a signal to a Supplier.
This triggers the processing of that batch, that gets reset, and we can wait
for the next batch.
We need to add the code to manage the Supply because a Timer::Breakable is a sort of extended version of a Promise, and we've already established that a Promise is insufficient to directly represent what we are after.
For that we would need something that represents a stream of timed events (like a Supply) each of which may be cancelled (like a Timer::Breakable).
Something like Timer::Stopwatch.
The example above could be re-written using Timer::Stopwatch avoiding a lot of the signal-management boilerplate:
use Timer::Stopwatch;
my $stream = Supply.interval(0.5).grep: { Bool.pick }
my Timer::Stopwatch $timer .= new;
my @batch;
react {
whenever $timer { # This implictly listens to $timer.Supply
say "Received a batch: { @batch.join: ' ' }";
@batch = ();
}
whenever $stream {
say "Queuing $_";
@batch.push: $_;
$timer.reset: 0.75;
}
}
# OUTPUT:
# Queuing 1
# Queuing 2
# Received a batch: 1 2
# Queuing 6
# Received a batch: 6
# Queuing 8
# Queuing 9
# Queuing 10
# Received a batch: 8 9 10
# ...
The difference here is that we have one [whnever
] listening directly to the
timer (which implicitly calls .Supply
on it to wait on its internal Supply),
and a separate one listening for events from the stream. When a new event
arrives through the stream, we add it to out @batch
and reset the timer.
This should make representing cases like this simpler, and for more complex cases it also includes ways to determine whether resetting a timer interrupted one that was running or not. It can be used as a count-down timer, like in this example, or as an open-ended timer. And just like it wraps around a Supply to represent the stream of events that go through it, it also wraps around a Promise to represent the lifetime of the timer, which can be irreversibly stopped.
Developers that have used Go before might find its interface to be familiar,
since Timer::Stopwatch was designed to mimic much of the interface of Go's
time.Timer
. And indeed, it has already proven to be very effective at
translating behaviours that have been written in Go into much
simpler Raku code that makes sensible use of the power provided by the Raku
classes we've been talking about.
Before I started writing Raku in earnest, I'd often spend some time here and there browsing through the documentation, marvelling at the seemingly endless landscape of types and classes made to represent a surprising array of what seemed to me to be very subtle differences. I wondered if I'd ever know enough to be able to confidently decide whether a Map, a Bag, or a Hash was the right tool for the job (let alone a SetHash or a BagHash).
As it turns out, one of the most illuminating things I learned while writing Timer::Stopwatch was that, while sprawling, Raku was not unmanageable. And its versatility means that you can use the tools that you are familiar with, and take your time to explore new things if you are so inclined. This, after all, is the essence of there being more than one way to do it.
As you do, you'll also get a feel for what the different types are meant to represent, and more importantly, what they are not. And with that a feel for the Raku language itself. I slowly feel like I'm coming to understand what feels Rakuish.
If you're getting started with Raku, the concepts I've mentioned in this article might seem confusing. It might seem sometimes like there are too many options, too many possibilities, and that might be intimidating. I know it was for me.
But I guarantee that Raku feels larger from the outside, and once inside its size feels less like a threat, and more like an invitation.
Who knows what lies waiting beyond the next click of a mouse?
Only time will tell.
Footnotes
-
This is the kind of thing I'm sad to say I've learned from experience. ↩
-
Unfortunately for this pun, Promise objects already have have ways to represent that they can be kept or broken, so perhaps a more accurate name in this case would be something like "Timer::Cancellable". But we wouldn't want accuracy to get in the way of a pun, now would we? ↩
-
We even have access to restrict what parts of the code are allowed to keep or break a Promise with the use of a
vow
. ↩
>
in front..stop with $timeout
will stop the timer when the embedded promise is kept, which will happen in 0.75 seconds. That will call the block, which will keep the promise... That will go to the first item in the react block... which will close the supply. You might want to say that in so many words ... o more. Even more so, when the next example is a bit more complicated.Please ping me back whey you have the final draft. You already have an account in WP, so please do the honors; I'll reschedule it according to needs anyway.