Skip to content

Instantly share code, notes, and snippets.

@jjatria
Last active December 4, 2020 06:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jjatria/62b570f9fba2acb80bb5d6b8afaf5d6f to your computer and use it in GitHub Desktop.
Save jjatria/62b570f9fba2acb80bb5d6b8afaf5d6f to your computer and use it in GitHub Desktop.
That Raku feeling

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

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

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.

Footnotes

  1. This is the kind of thing I'm sad to say I've learned from experience.

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

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

@JJ
Copy link

JJ commented Nov 29, 2020

  1. time up to a future event
  2. drop the dash in well-supported
  3. link "related ones" to something meaningful.
  4. Promise from the runtime system more than the compiler maybe?
  5. Maybe change "one-second" to "one-second-passed" or something like that?
  6. As long as you're saying "I promise", you could add another pun along the lines of "this is a promise that's non-cancellable" or something like that
  7. I don't think footnotes are working here. You might want to use blockquote, with the > in front.
  8. You should probably clarify a bit more the code that introduces Timer::Breakable. I kind of gather that .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.
  9. Maybe not true, since you're listening to the timer later on... So a Promise has an internal Supply too?

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.

@jjatria
Copy link
Author

jjatria commented Nov 29, 2020

@JJ: I think I've made most of the changes you suggested. Here are some comments on them:

6: As much as I'd like to milk this pun for more, I feel like it would break the flow of the text, so I left it alone.

7: Yeah, Github doesn't seem to like footnotes, it seems. I'm not sure about blockquotes, because I'm not really quoting anyone. An <aside> would work, but I guess this all depends on what sort of styling the WordPress instance allows. Does that support footnotes?

9: I wasn't sure what you meant by this. I clarified that doing

my $timer = Timer::Stopwatch.new;
react whenever $timer {
   ...
}

implicitly calls .Supply on the timer and listens on its Supply. As for whether a Promise has an internal Supply... I guess you'd know better than me. But I'd be really surprised if that was the case :O

As for the other WordPress thing... I just realised that I do seem to have an account (that I had completely forgot about), but I don't think I can use it to post on https://rakuadventcalendar.wordpress.com. What happens next?

@JJ
Copy link

JJ commented Dec 3, 2020

Sorry, i was pinging you in the other place when I had this ready here. You can upload it as a draft, leave it for me to schedule it, if that's OK for you. Thanks!

@jjatria
Copy link
Author

jjatria commented Dec 3, 2020

@JJ: I'm confused. Upload it as a draft where? I don't think I have access to anything but the public side of the Raku advent blog. Or do you mean somewhere else?

@JJ
Copy link

JJ commented Dec 4, 2020

Well, that blog. Send me you wordpress ID so that I can invite you there, and then you can upload it yourself or leave it to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment