User input with timeout doesn't work as hoped
Asked Answered
S

2

6

Why don't loop cycles work as before after a timeout (no more printed user input)?

#!/usr/bin/env raku

loop {
    my $str;
    my $timeout = Promise.in( 5 ).then({
        $str = 'Timeout';
    });
    my $user = Promise.start({
        $str = prompt '>';
    });
    await Promise.anyof( $timeout, $user );
    if $str eq 'q' {
        last;
    }
    say "[$str]";
}
Sock answered 29/4, 2020 at 6:32 Comment(0)
B
6

It's because you're talking to the wrong call to prompt, which is in a promise which has closed on a different variable $str. The second and later calls to prompt block, waiting for the first call to finish. But the $str that is to receive the value of the first call is out of scope, so nothing happens.

That sounds very strange, but here's an experiment you can run to help your intuition along while I dissect it more fully: run the script, wait for the timeout, then enter q twice in rapid succession. The script quits after the second one. Why?

On the first loop, we declare a variable $str which I'm going to call "$str number 1" and create a Promise that closes over $str number 1 and calls prompt. prompt attaches to STDIN and doesn't return until it sees a newline. When the timeout expires, that call to prompt is not interrupted. It's still running. Still waiting. The promise it's attached to (let's call it $user promise 1) is still active even though the variable $user is about to go out of scope.

On the second loop, we declare a new variable $str ("$str number 2"), create a Promise that closes over it, and call prompt again. But another call to prompt is still using STDIN, so the new call blocks and waits for STDIN to become available. If you type something now, it will be seen by the original call to prompt, which was attached to $user promise 1 and closed over $str number 1.

$str number 1 is updated when prompt returns, but it doesn't matter because you stopped looking at it. The if $str eq 'q' conditional is going to inspect $str number 2, because that's the variable that was declared in the current loop.

The second call to prompt then immediately asks for input, and if you type q before the timeout expires, it updates the version of $str that it closed over, $str number 2. Since that's the one your conditional is inspecting, the loop terminates.

Every timeout starts a new prompt without terminating the old one, which means that the input the user types is never attached to the same $str that you are inspecting. Even if you inspect the original variable, the subsequent calls to prompt still happen and will keep prompting even after execution has left the block.

Since prompt doesn't have a way to specify a timeout and Raku doesn't have a way to "kill" scheduled Promises, I don't think you get to solve this problem with prompt.

Baerman answered 2/5, 2020 at 0:56 Comment(0)
A
3

The logical issue in this code, is that the timeout Promise will trigger after 5 seconds, even if someone has entered something in the previous iteration. And so it will set $str at seemingly random times.

There is a simple solution: just make sure that you do not assign $str in the timeout code if it has already been set:

$str //= 'Timeout';

For this example, it doesn't really matter, but generally you don't want code to be executing willy nilly, so it would be better to actually de-activate the Promise. Unfortunately, you cannot do that with the Promise interface. But the Promise.in method is actually a wrapper around the ThreadPoolScheduler.cue method, which does return a Cancellation object (https://docs.raku.org/routine/cue).

Alteration answered 29/4, 2020 at 9:22 Comment(3)
When I replace the 6. line of my example with $str //= 'Timeout' I don't see any changes in the outcome of the script.Sock
Then maybe you should describe the outcome of your script more precisely?Alteration
After a triggered timeout a user-input of q no longer quits the script even with $str //= 'Timeout'.Sock

© 2022 - 2024 — McMap. All rights reserved.