Duration has no Rem implementation?

A correctly implemented interval timer should have exactly the behavior you're asking for, and if it doesn't that seems like a bug. I'm under the impression that the timer implementation I linked to has that behavior.

I just tested it, and it does, with good accuracy and no drift:

use std::thread::sleep;
use std::time::{Duration, Instant};

use async_io::Timer;
use smol::prelude::*;

fn main() {
    smol::block_on(async_main())
}

async fn async_main() {
    let mut timer = Timer::interval(Duration::from_millis(250));
    let start_time = Instant::now();
    while let Some(_) = timer.next().await {
        let d1 = start_time.elapsed();
        sleep(Duration::from_millis(12)); // Work
        let d2 = start_time.elapsed();
        println!("Loop iteration started {:?} from start, work finished {:?} from start", d1, d2);
    }
}
Loop iteration started 250.211495ms from start, work finished 262.403826ms from start
Loop iteration started 500.31191ms from start, work finished 512.525258ms from start
Loop iteration started 750.317718ms from start, work finished 762.403713ms from start
Loop iteration started 1.000112309s from start, work finished 1.012208147s from start
Loop iteration started 1.250324057s from start, work finished 1.262532751s from start
Loop iteration started 1.500269808s from start, work finished 1.512473307s from start
Loop iteration started 1.750332066s from start, work finished 1.762544106s from start
Loop iteration started 2.000321639s from start, work finished 2.012547658s from start
Loop iteration started 2.250304702s from start, work finished 2.262510903s from start
Loop iteration started 2.500315309s from start, work finished 2.512509487s from start
Loop iteration started 2.750285747s from start, work finished 2.762358475s from start
Loop iteration started 3.000210088s from start, work finished 3.012366125s from start
Loop iteration started 3.250321125s from start, work finished 3.262510832s from start

Notice that the loop iterations happen every 250ms, even though the loop does 12ms of work.

2 Likes

This crate is useful, but does this crate (or any crate for that matter) suffice as an argument against implementing Rem for Duration?

I strongly disagree on this semantics. When you divide timespan with timespan the result should have unitless scalar type like f64. To get timespan by dividing other timespan you should use unitless scalar types like f64 or u64 as a divisor. Both are trivial to implement with current operator semantics.

Sorry, I totally misread the post.

I think you’re mixing up / and %. The quotient is unitless, but the remainder is a timespan.

2 Likes

Actually, it totally makes sense to define modulo by zero to return the ‘dividend’ operand even for integers.

1 Like

The mathematical modulus operation and the programming remainder operation are subtly different.

So while $5 \equiv 5 \pmod 0$, 5 % 0 is still undefined.

The definition of so-called ‘remainder’ you linked does not even cover negative divisors meaningfully; why would it be meaningful to apply it to zero? The post I linked is agnostic as to the rounding mode.

And who is to say that whoever performs % of two time duration values wants the so-called ‘remainder’ rather than the so-called ‘modulo’? (I say ‘so-called’, because I doubt actual number theorists use those terms in such a way.) We can interpret a % b as ‘measure the interval from zero to a, resetting the timer to zero each time we pass a multiple of b’, with which it makes perfect sense to set a % 0 == a. And this is basically the most common T-definition.

There's a reason if the trait is named Rem. Rust's % operator is defined to be the remainder operator, and at least the stdlib should be coherent with that.

2 Likes

Rem is just a name. So is Add, which is implemented for String, despite the operation not being commutative, nor admitting inverses.

If you insist on Rem meaning ‘remainder’, setting x % 0 == x is compatible with any remainder rounding mode: a - (a % b) stays a multiple of b, approximating a in the same manner that it would for any other b. It’s a perfectly well-defined remainder of approximation by a multiple of b.

You may say ‘but Rem is supposed to be the remainder of division, and there is no way to meaningfully perform division by zero’. But the original post asked only for impl Rem<Duration> for Duration, without any impl Div<Duration> for Duration to go with it (and this is not currently implemented either; there is only impl Div<u32>, which divides a time duration by a scalar). So what was asked for might as well be a ‘remainder’ in its own right, without being paired with any quotient.

In fact IMO that was an error, but we have to live with it due to backward compatibility. Anyway, something being the stdlib doesn't mean it was a good design choice.

That ignores the restriction 0 <= a % b < b though. There's no value that satisfies it if b = 0.

1 Like

I actually agree, but it is not me who brought up precedent in std as relevant and binding. If this argument is to be taken seriously, then it is worth pointing out that precedent is not actually as clear-cut as it may seem at first. So unless its application is to be subject to arbitrary whims, I believe it should be either supplemented or dropped as an argument. I tend to be distrustful of precedent in itself as a justification for anything, so I would rather do the latter.

There is no value that satisfies it for b < 0 either, as I mentioned before. This is not particularly applicable to durations (which are always non-negative quantities), but it does suggest that the true condition that is worth generalising to cover b = 0 is perhaps a little less naïve than this.

Rust has, in general, managed to avoid some of the readability hazards that often plague code using operator overloading, in part because people see names like Add and Rem and treat them as guidance to implement a semantically similar operation. There's a reason we don't call the operator traits something like PlusOp and MinusOp and StarOp and SlashOp; such names would not provide the same guidance towards semantically aligned meaning.

4 Likes

I don't think you really want to use Rem for this use case. You should just sleep for max(target_interval - (current_time - last_poll_time), 0).

Note that this doesn't quite work because Durations panic instead of going negative.

Right - on the other hand, Duration provides saturating_sub, which would do exactly this in one step less. Or checked_sub and don't call sleep at all if it's None.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.