A few times now I’ve [bumped][1] into [situations][2] where a method takes a std::time::duration::Duration as a parameter but has ambiguous or strangely defined behavior in the event that the underlying signed value is negative. The accepted solution seems to be to silently convert negative values to zero, which seems to violate the principal of least surprise.
The fact that a duration can be negative is a bit odd to me; in my mind, a duration is always a positive value. The difference between today and two days ago is always “two days”, just as the difference between 5 and 3 is 2, not -2. However, Java 8 (and Joda Time) offer [the ability to negate a Duration][3], so clearly this is an established practice.
I have two questions:
What’s the use case for a negative Duration?
It seems as though there are a number of APIs that would benefit from the addition of a unambiguously positive variant of Duration (e.g. UnsignedDuration) to the time library. Is there a reason not to add one? I’d be happy to work on this.
From my understanding Duration is Instant_A - Instant_B. Duration is a highly mechanical, and universal. One thousand milliseconds will be one thousand milliseconds regardless if the instant used to extract it where captured on February 29th or February 28th. They can be negative if Instant_A < Instant_B.
However, what you are describing with two days is more akin to Period from JodaTime. Period is difference between instant in human described terms (e.g. days, months, years) which can vary in length depending on context. Periods are calculated by adding human time units which depend on context. If you say add two days to 2014/12/31 23:00:00 you expect to get 2015/01/02 23:00:00. This can take an arbitrary amount of mili/nano/seconds depending on whether the year is leap, how many days there are in months and other complex date malarkey.
And even in Period there is a concept of forward and backwards time. A negative two day Period means - two days ago, while positive two day Period means day after tomorrow.
Use cases of negative durations are easy to guess. You want to calculate difference between dates and add that difference to some other variable even if it’s negative.
You can easily allow to create some kind of abs_dur function, which will return an always positive duration.
In the case of the time crate as we have now, Timespec - Timespec yields a Duration, regardless whether the minuend is later than the subtrahend or not.
Allowing a negative Duration as a parameter of, say, your sleep() API can be justified if you define your sleep() API in such a way that it sleeps at least the specified amount of time. That is, a negative Duration parameter tell the API to sleep for the minimum possible time.
This isn't just playing with words because it's impossible to implement an API which guarantees to sleep exactly the same amount of time in general. I guess many APIs that accept a Duration can be defined in a similar way.
However, what you are describing with two days is more akin to Period from JodaTime.
What I was describing is closer to the TemporalAmount class, which represents an "undirected" (neither forwards nor backwards) amount of time. My concern is not the distinction between "universal vs human time", but rather with the ability to express the requirement for an undirected number of milliseconds (or any other unit) in an API.
I appreciate the need for a way to represent the difference between TimeSpecs. It seems like Duration is well suited to that. But I think there is also a distinct need for a way to represent a quantity of time that's completely detached from a "start" or "end".
Allowing a negative Duration as a parameter of, say, your sleep() API can be justtified if you define your sleep() API in such a way that it sleeps at least the specified amount of time. That is, a negative Duration parameter tell the API to sleep for the minimum possible time. ....
The compromises that people are making to use Duration to meet this need are reasonable, but one of Rust's greatest selling points is its strong and expressive type system. I think there's a lot of value in being able to write a method whose signature communicates exactly what it's asking of the user. Relying on the developer's mastery of the documentation to understand which parameters may be quietly re-interpreted and when feels much more like programming in C to me.
True, but adding UnsignedDuration sounds like doubling the API for marginal gains. The use case of negative Duration seems more universal, while having strictly UnsignedDuration sounds like it won't be usable in all cases.
For example what if in a Calendar I need to mark dates that are overdue? Without negative Duration I can't tell if element is 100 days past or till the due date.
It's clear to me that Duration is intended to be a directed difference between two Timespecs. I'm proposing that we need a separate class to represent an undirected quantity of time. It doesn't have to (and probably shouldn't) be named UnsignedDuration.
I am in no way proposing that we get rid of Duration as it stands today. I'm suggesting that we create a new struct for the family of uses for which Duration is a poor fit.
I fully understand your concern. I guess the POSIX designers shared the same concern with you when they defined read(2) so that it doesn't take a signed integer as its length parameter.
However I'd claim that, for all use cases of what you call UnsignedDuration, the semantics of the API has to be "at least the specified amount of time" type (because you can't really control the flow of time,) and then treating a "negative" duration as zero is nothing like "quiet re-interpretation." It should naturally follow from the word-by-word interpretation of the specification of the API.
Also, the idea of "undirected" Duration sounds odd to me. In the original thread
you wrote
"Two days ago" is still a duration of two days.
This is almost denying the whole idea of negative numbers which dates back to 7C AD. Do you think game devs have to model a moving object with a pair of its position and "a pair of absolute value of its velocity and a flag taking two possible values {left, right}," because moving left by 10 m/s is still a move with 10 m/s? I hope you can agree with me in that "-10 m/s" is appropriate here.
I must admit that when I first decided to post my suggestion, I did not imagine that it would result in someone accusing me of dismissing the existence of negative numbers.
I think it's valuable to be able to model data as is most natural for each domain. Just as I would prefer to use an unsigned number of meters to represent a person's height, I would also want use an unsigned temporal type to represent an amount of time in the future. At present I cannot because Duration is inherently signed. I'm not asking to change Duration, I'm volunteering to add another option to our libraries.
The problem is you are essentially asking “lets double all API that interacts with Duration in time crate”. You do understand the amount of work you’d need to do support this use case?
Every operation that takes/accepts Duration will need to be doubled for UnsignedDuration. There are already a lot of operations that will have duration as part - for example Duration + Instant = Instant. Now there will be need for UnsignedDuration + Instant = Instant, etc.
You need to prove that adding Unsigned Duration is real requirement (by demonstrating the use case is critical, e.g. demonstrating either that other libraries have it or that a lot of boiler plate is added due to it’s introduction) or barring that a way for API to be doubled without a significant cost to new users learning it. For example maybe Phantom types can help out?
API creators can’t really make EVERY possible API usage safe. For example height you mentioned earlier as unsigned length would be better suited for human height as an unsigned int that is between (0-400)? Surely no human taller than 400 cm will ever be born due to the way our organs work. It would represent human height really well. But it’s not a sensible use case for height.
This might change if Rust adds polymorphism and UnsignedDuration extends Duration, then it would be easier to create API without making API grow out of proportions.
I don't believe that this is necessary. Just as u32 and i32 are able to coexist without developers having to write two versions of every function, one would only use the unsigned type where it makes sense. It should also be possible to write a trait to abstract away which is which, similar to how the common functionality of u32/i32 can be accessed via the Int trait and friends.
It should also be possible to write a trait to abstract away which is which, similar to how the common functionality of u32/i32 can be accessed via the Int trait and friends.
While true, the Int trait also uses copious amounts of macros to allow such duplication (technically, it's octoplication - since there is u8, u16, u32, u64, i8, i16, i32, i64). See source.
With Int, it's a necessity (because unsigned is useful when indexing array and describing memory addresses).
Currently this:
(inst_a - inst_b).abs() // returns non negative Duration like JodaTime
already covers 90% of use cases.
Summary:
Duration Covers almost all use cases, is established convention is simpler to implement and leaves very few uses cases unsafe.
UnsignedDuration is more complex to implement, is non-convention, creates new probably unknown problems (e.g. what is:
You could say the same of u32/i32. "If anyone passes a signed integer where it doesn't make sense, I'm going to take the absolute value." As you say, this isn't unsafe per se, but it violates the principal of least surprise from the API user's perspective. The only way to realize that you've passed something illegal is to notice unexpected behavior at runtime.
These don't seem that mysterious to me. The arithmetic would be very analogous to the way it's handled for u32/i32.
I appreciate the value of following a convention and I also recognize that this is not a trivial change. I suppose I'll just use milliseconds: u32 in my code and be done with it.