[Pitch (back from revision)] Clock, Instant, and Duration

Howdy y'all; it is that time again, to talk about time.

We gathered a lot of great feedback and after sifting through it, weighing the pros and cons, and some prototyping, we have revised the proposal for Clock, Instant, and Duration.

This proposals' main aim remains the same: to provide a set of types to express scheduling work via temporal control. Clock, Instant, and Duration will remain nearly the same; but per issues that have been brought up we will no longer be lowering Date.

Clock itself remains mostly the same except one small, but important, addition that was brought up to me in private during the review - and we tend to agree that this is a pretty important feature that was missing for power efficiency. That feature is tolerance. For repeated calls, the tolerance can allow for kernel level scheduling of wake-ups with a grace period of how accurate the sleep needs to be. Which in turn, when it is supported, the clock itself can offer a way to express from the caller to say; this is a very important sleep to be accurate upon, or it is a sleep that can have some level of drift to ensure coalesced wake-ups happen. This parameter will be optional. In the concrete clocks we are providing it will be nil by default (a dealers choice style parameter that the underlying implementation may decide on numerous factors for what type of reasonable tolerance can be applied).

Next up is Instant. It was brought up that some Instant types may need to express duration differently than just an integral time. Notable examples of this are things like clocks based on a GPU where the duration is frames, or a clock based on media where the duration is a rational. These are useful concepts that we should have the flexibility to handle while still retaining the ease of use for common integral values. After prototyping this, we feel that we have meet that balance while preserving the ergonomics that having just a shared concrete duration type serves.

Finally; Duration this type has not changed, except two points. It now adopts a DurationProtocol which is the constraint for the associated type on Instant. Additionally the static methods for calculating hours and minutes are removed (again).

The response was quite robust with regards to Date and WallClock - there were definitely some rough edges. Specifically the name Date was not felt that it was appropriate to lower since it can cause confusion between a computational storage date and a human calendrical date. We heard this and we understand the objection. Ultimately we decided that it is best to leave Date in the contextual frame of the home of calendrical APIs; Foundation. While looking at it from the frame of reference of Foundation, the name is reasonable when considering the material history of that type. It makes sense after getting over potential issues with regards to how it is a type devoid of timezone and calendar. Date is intended to be used with Calendar and we concur that moving, renaming, or altering the storage of Date is not ideal. The primary driver for this issue is that of serialization. To alter the serialization method we would need to change considerably more, at considerably more risk, than what we think is reasonable for inflicting on Swift. That all being said; there is still a use case for a clock that has a type that can interface with calendrical systems. Scheduling a task to be woken up at 4PM tomorrow is perfectly reasonable and useful. We feel the discussion of Date and and a clock that uses it is an integral part of this proposal; so included in the altered proposal is a section devoted to a new type, UTCClock, in Foundation. Directly related to the feedback with regards to Date; one major omission of the Foundation (and Darwin) APIs is a way of determining historical leap seconds - I definitely invite folks who have raised concerns about the accounting of leap seconds to weigh in.

Last but not least; we felt that the names of the concrete clocks previously proposed were quite opinionated with a bias to Darwin nomenclature. The names on Darwin for C APIs (as well as other BSD like platforms) take the meaning of "monotonic" as continuously increasing even during sleep of the machine, and "uptime" to be suspending increases while the machine is asleep. Whereas the names on Linux based operating systems are exactly the opposite. We feel that names describing the behavior are much more appropriate for a language that is being developed to be cross platform than picking names from precedent of other APIs. We have renamed MonotonicClock to be ContinuousClock and the UptimeClock to be SuspendingClock. To this end we have included some of the alternatives we have considered during this renaming process to aide the discussion by giving some clarity on why we picked what we did.

Since the proposal exceeds the length of posts (perhaps I am a bit too wordy) the most recent revision can be found here.

48 Likes

Hey Philippe!

I'll cut right to the chase: I love these changes. This is looking really good and if it were merged as-is, I'd be content.

What comments I do have are very minor ones that I don't consider blocking in any way:

  1. The proposal has the associatedtype Interval, which conforms to DurationProtocol, and a concrete Duration type that is used as an Interval. I find the naming duality a little hard to follow sometimes, especially because I sometimes gloss over Interval and mentally read it as Instant and then get confused. I think overall your rationale for choosing the names you did is correct, so I don't really have a better suggestion right now. Just the observation that there's some friction there.
  2. I understand the choice about UTCClock existing in Foundation so it can take advantage of Date (and thank you for keeping that in Foundation!). I do think it's a bit unfortunate that we'll need Foundation to get the device's wall clock, since not every user of the standard library will be able to import Foundation. Is there something that could be done about having that in the standard library, but Foundation adds extensions to its Instant type (as with the other instant types) to make it easily translatable to Date?

These comments are relatively minor ones. I really appreciate the time and thoughtfulness you've put into this, and especially that you (and everyone you're working with) have heard and incorporated the feedback presented here. :smiley:

21 Likes

So many improvements; the only thing I can complain about now is the whole detailed design is still a single giant Prior Art section (according to the heading levels). :wink:

heh, yea that probably can be altered in the heading levels - along with some other house-keeping I will adjust that.

1 Like

Updated the proposal with some minor grammar fixes, some clarity refinements, and some better section delineation; non-functional changes.

3 Likes

Generally I'm quite positive on this.

Something about DurationProtocol is deeply weird, though--it has nothing to do with durations. The actual protocol requirements essentially make it describe an Archimedean group viewed as a module over the integers (which is isomorphic to a subgroup of the real numbers), and equipped with a few weird division operations. This is both an oddly specific thing to give a stdlib protocol name to, and also a very general thing to use the name DurationProtocol for. I believe that some refinement is necessary here either in the form of narrowing down the protocol requirements to what is actually essential, choosing a more suitable name, or both.

I think that the correct resolution here is to split the protocol into a couple pieces that cover more common abstractions--one is the notion of a module or vector space where you can add, subtract, and multiply by a scalar, another is comparable, and I'm not totally sure how the division operations should fit in--and then use their intersection as the generic constraint. I'm open to other ideas, though.

I'm also a little bit wary of using * and / for the heterogeneous multiply-by-scalar and inverse, because experience has shown that this can lead to somewhat surprising type inference behavior, especially if any ExpressibleByXLiteral conformances get involved. I think I would prefer to reserve those for homogeneous operations, at least in the standard library.

This is a lot of little details for one specific piece of the pitch, but overall I think that it's a strong proposal, and I am confident that we can work these out.

10 Likes

I'm going to push for allowing heterogeneous *, from experience using Rust's Duration type. It's extremely common, especially when writing tests, to multiply a Duration by a constant. And then once you have * with a scalar it's weird not to have / as an inverse-of-sorts. I'm not so worried about literals because most Durations would not be expressible by literals anyway; it's always better to specify units unless it is something like "number of frames". That's specific to Durations, though, not vector spaces in general.

This might suggest that "vector space" deserves its own protocol, but DurationProtocol would still be the one that introduces the operators.

7 Likes

The ending signature of DurationProtocol is a bit weird I will grant that. The reasoning is that those requirements are there to ensure the fast paths for calculating certain durations are there; namely of which the slop calculations for timers. If we omit those paths then the duration types don't even really need a protocol they can just be a requirement on Instant.Interval to be Comparable, AdditiveArithmetic, and Sendable.

If it really just seems too outlandish to have the duration protocol like this then we must find a way to ensure a loop calculating an effective multiplication is reasonably performant to account for the slop/drift/tolerance calculations.

The other algorithm that I know will need these requirements is a backoff calculation. Again the multiplication can be done via looping addition I guess...

Another (not fully convinced it is a good idea) is a "homing-in" style sleep; so given a specific target, sleep half, then sleep the remaining half etc until you are close. This one might be hard without division.

So the change would be this:

public protocol InstantProtocol: Comparable, Hashable, Sendable {
  associatedtype Interval: Comparable, AdditiveArithmetic, Sendable
  func advanced(by duration: Interval) -> Self
  func duration(to other: Self) -> Interval
}

and a flat out removal of DurationProtocol.

4 Likes

Just a tiny bit of naming that struck me as slightly odd, but this may be a matter of personal preference.

The associated type for the Clock protocol is called Instant, constrained to InstantProtocol:

public protocol Clock: Sendable {
  associatedtype Instant: InstantProtocol
  
  var now: Instant { get }

The associated type for InstantProtocol is called Interval, constrained to DurationProtocol:

public protocol InstantProtocol: Comparable, Hashable, Sendable {
  associatedtype Interval: DurationProtocol
  func advanced(by duration: Interval) -> Self
  func duration(to other: Self) -> Interval
}

I would have expected something more like

public protocol InstantProtocol: Comparable, Hashable, Sendable {
  associatedtype Duration: DurationProtocol
  func advanced(by duration: Duration) -> Self
  func duration(to other: Self) -> Duration
}

Edit: oh, I see, this would clash with struct Duration.

Also, I'm a bit confused by the use of the Protocol suffix: some protocols are denoted by a Protocol suffix (e.g., DurationProtocol, InstantProtocol), while others aren't (e.g., Clock). Is there some general rules behind these naming decisions, or is it just this proposal? Sorry if this has been answered already in the pitch thread.

I think it has to do with wanting the bare name for a type (Instant, Duration), and not being able to overload the name. There will be no concrete implementation of the Clock protocol provided by the stdlib named "Clock", so there's no need for the "Protocol" suffix.

To be clear, I'm not arguing that they shouldn't exist, but rather it's possible that they shouldn't be spelled * and / on a VectorSpace/Module-like protocol. The problem isn't with Duration, but rather with other types that one would like to conform to such a protocol, especially complex and quaternions (which are vector spaces over the reals) and Gaussian integers (which are a module over the integers). For these types, it's natural for both the base field/ring (the reals or integers) and the vector space to have the same literal type, which makes expressions like 2 * z ambiguous. In some sense, this ambiguity isn't a problem, because it doesn't matter--the whole reason why it makes sense to have the vector space be expressible by a scalar literal in these cases is that mathematically the two interpretations are equivalent. But we don't (currently?) have the language tools to communicate that to the compiler.

This seems like a reasonable resolution.

3 Likes

"Zeno's Algorithm"

6 Likes

I’ve used zenos algorithm for high accuracy sleep via other systems but is it even wise for async/await? That is the open question.

Would it, thought? Couldn't some clock's Instant just declare public typealias Duration = Swift.Duration?

I can't see a lot of examples of when this would clash in practice, and the somewhat extranous Swift. prefix would be constrained to that single type alias declaration, which is a very small burden on library authors, which wouldn't surface to end user.

Or am I missing something?

1 Like

The reasoning for the name being Interval was that it is probably pretty common for implementors of clocks to use Duration as the Interval type. It would then mean that developers would immediately get some potentially confusing errors. I am not sure type alias Duration = Swift.Duration is immediately obvious of a solution to use that. Whereas writing func advanced(by duration: Duration) -> MyInstant is pretty straightforward.

Sure :-)

For those interested, I have re-opened a new PR w/ some of the changes associated with this pitch: Clock/Instant/Duration by phausler · Pull Request #40609 · apple/swift · GitHub

It is not yet complete; I need to implement the cooperative executor and adjust a few things that are internal implementation details with the dispatch side of things.

2 Likes

So I have been looking into this because the naming discontinuity does bug me a bit too.
Doing so does not harm the general use of clocks; which quite honestly will be a VERY high majority of the code that will touch this stuff. The few cases where it will matter are the cases where someone is implementing a clock. It does make the implementation of clocks a touch more obtuse, but that difficulty is traded off by making the names line up nicer.

In short: if folks feel strongly about this - it is a tradeoff that we are willing to make.

6 Likes

Hi @Philippe_Hausler!

This is absolutely fantastic. I've let some time go by after reading the document once, and on second reading it's even better! I agree with @davedelong that the changes overall strengthen the final design significantly.

A few questions and points of feedback:

I agree with others that the Interval versus Duration terminology is weird. Additionally—and I don't think this has been mentioned—recall that in earlier versions of Swift Interval was a standard library type that got coalesced into Range: it would be nice not to resurrect that name for something that's totally different.

If another word would be useful for the associatedtype, might I suggest TimeSpan, which is obviously time-related unlike Interval.

However, I think it's quite fine not to use two different words: indeed, the standard library ExpressibleBy* types define associated types that have the same name as global type aliases (e.g., StringLiteralType is an associated type of ExpressibleByStringLiteral and also a global type alias).


I agree with @scanon re the weirdness of the DurationProtocol: if it's not needed, that would be fantastic.

I also have some questions regarding the operators it (and the concrete type Duration) exposes, which center around this: is there an implementation reason that multiplication by arbitrary integer types is a requirement, while division of two conforming types produces a Double result? It would seem logical that multiplication would be by Double values also (which as you know can represent all consecutive integers exactly up to 2 ** 53, and which I notice the concrete Duration type supports)... Do the integer overloads have to do with what you were referring to in your reply about "ensur[ing] fast paths for calculating certain durations"?


I think I understand the usefulness of the newly pitched minimumResolution API. Two minor points about the name:

  • First, I think there are two subtly different interpretations a user could have: either (1) "great! this clock guarantees that it's at least that accurate"; or (2) "oh! I shouldn't expect two points in time less than that to be distinguishable." Maybe I'm confusing myself and these two are essentially the same statement, but I think this is boiling down to accuracy versus precision, and I'm not exactly sure which one we're trying to represent here and whether there is a common term-of-art in computer timekeeping for it other than "resolution." The proposal also uses the term "granularity" to describe the API—I wonder if there's way to clarify this not only through the explanatory text but in the name of the property itself.
  • The standard library, usually eschewing abbreviations, has adopted min and max as the spelling of "minimum" and "maximum" throughout, with the sole exception of the floating-point IEEE "minimum" and "maximum" operations that are deliberately named distinctly from Swift.min since they handle NaN values differently. Even if we stick to the term "resolution," the API would be more consistently named minResolution.

I agree with @davedelong that it's (only a bit) unfortunate that we'd not have a standard library wall clock. I think the revised approach of not putting it in the standard library makes a lot of sense when interfacing with an unchanged Foundation.Date type is essential, while still allowing for use with these modern non-calendrical APIs via conformance to ClockProtocol. Overall, I like this approach very much so long as we don't close the door to something like the approach which swift-system takes with FilePath with a potentially separate system clock project (obviously without necessarily committing to such an approach)—based on what I'm seeing in the proposal, there shouldn't be any barriers to third-party libraries tackling this space, correct?

9 Likes

I am very much in favor of shifting responsibility to new packages rather than Foundation. Foundation is way too monolithic, and it is extremely difficult to tell whether code uses platform-specific behavior from it.

3 Likes