Day 12: That Raku feeling

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:

Counting from the past

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.

Counting to the future

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-passed;
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.

A moving goalpost

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.

Promises and pie crust

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 (I’m sad to say I’ve learned this from experience). 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.1 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”.

Where there is one there are many

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.2

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 for every demand

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.

Wrap around the clock tonight

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 whenever 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.

A feel for Raku

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.

Notes

  1. 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?
  2. 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.

4 thoughts on “Day 12: That Raku feeling

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: