SE-0329: Clock, Instant, Date, and Duration

Hello, Swift community.

The review of SE-0329: Clock, Instant, Date, and Duration begins now and runs through November 15th, 2021.

Reviews are an important part of the Swift evolution process. All review feedback should either be on this forum thread or, if you would like to keep your feedback private, directly to the review manager. If you do email me directly, please put "SE-0329" somewhere in the subject line.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at:

https://github.com/apple/swift-evolution/blob/master/process.md

As always, thank you for contributing to Swift.

John McCall
Review Manager

8 Likes

I'll save commenting on the wider proposal until I've had a chance to read through in more depth, but on the specific point of the naming of Date: I know it was of contention in the pitch thread and I happen to agree it's not a mistake that should be carried forward. Is there any technical reason it could not be given a new name, have a typealias of:

@available(*, deprecated) // with the correct stdlib version
public typealias Date = NewName

and change the Clang importer to map NSDate to the new name? It would remain bridgeable with NSDate under this scenario. Aside from a bunch of deprecation warnings in Swift code that uses Date currently (easily addressed by a find & replace), it seems like this would provide a fairly clean road forward.

15 Likes

First off, I'm quite surprised this is going up for review already. There was only the single pitch thread, and (from what I saw) the public proposal text was never updated for further feedback. This review seems premature.

That being said...

What is your evaluation of the proposal?

-1. This proposal is a good start, but I fundamentally object to the inclusion of moving Date into the Standard Library and do not believe the proposal should move forward as long as that's part of it.

Is the problem being addressed significant enough to warrant a change to Swift?

Yes. The addition of the Clock and Instant types is long overdue. I also really appreciate @Philippe_Hausler's incorporation of custom clocks (and the accommodations to the various protocols to support them). Those will be enormously beneficial. I'm excited to use them (and delete my own versions of them!).

Does this proposal fit well with the feel and direction of Swift?

Overall, mostly. The Clock and Instant additions are great and we should absolutely accept those. I have two major specific concerns:

  1. The presence of .minutes(...) and .hours(...) on Duration are going to be abused by developers wanting fundamentally different concepts. I do not think they should be there, because they look like the right thing, but might not actually be what you want. You think "ah, I want to show something to the user 3 hours from now, therefore I do myWallClock.now + .hours(3), without realizing that this is usually the wrong approach to be taking.

    Because of this, I would like to see Duration limited to only .seconds(...).

    At a broader level, I can see potential trouble for having a single Duration type that's common to all Clocks. With the ability to create custom clocks, it seems reasonable that I'll be able to make a RegionalClock that is calendar, timezone, and locale-aware. For such a clock, I don't think I'd want to use common Duration type, because of the complexities around calendar-aware durations. Thus, the requirement that my RegionalInstant type be forced to advance by a Duration seems counter-productive. I'd instead want to make sure that my type only supports a calendar-aware Duration. This leads me to believe that perhaps Duration should be another associated type on the Clock protocol. I know this came up in the pitch thread, but I believe it needs further examination and is part of why I'm surprised to see this move to review phase already.

  2. Date should not be moved into the standard library. There is universal agreement that "Date" is the wrong name for the type. Even the manager of the Foundation team concurs. Here, with the introduction of new timekeeping APIs, we have the opportunity to correct this, and we're instead proposing to double-down on this very confusing name.

    What I keep coming back to is asking myself this question: "do we think Swift's popularity has peaked?". I don't believe so. I sure hope not. I believe that interest in Swift will continue to grow, and that our current developer community size is a fraction of what it will eventually be.

    To me, that strongly implies that we will have more new Swift developers than what we currently have, and every single one of them will need to be taught: "This thing that's called Date is not actually a date".

    We can do better than this, if we're willing to accept some minor, short-term discomfort for the sake of longer-term pleasantness.

    The proposal discusses an alternative:

    It is true that there will be short-term confusion if we have both WallClock.Instant and Date, or if we deprecate Date in favor of WallClock.Instant.

    But how does that short-term confusion stack up against the confusion that millions of future developers will face every time they encounter this incorrectly-named type?

    Two of Swift's stated goals are to be "safe" and "expressive". What is safe and expressive about having a confusing API because we gave a concept the wrong name? We claim that "Swift benefits from decades of advancement in computer science to offer syntax that is a joy to use". I submit that renaming Date is an opportunity to contribute to that goal, just as the rest of our language and APIs have. We owe it to every current and future developer who uses this language and library to give them the best-possible thing we can make, and I do not believe the Date name is capable of fulfilling that goal.

    I think the way forward would be:

    • Create WallClock.Instant as a separate concept from Date
    • Deprecate Date (but not NSDate)
    • Teach the importer to create both WallClock.Instant and Date-taking versions of methods
    • Do this over a major release or two if we're really worried about churn and confusion

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

In other comparable languages, the term Date has been eschewed entirely. Any locale-aware language prefers terms like Moment and Instant for the "instantaneous point in time" concept. The only major exception I can think of to this is JavaScript, but that's a false positive because the JS definition of a date is intrinsically tied to a Gregorian calendar. All major languages use names like ZonedTime or LocalDate for calendar-aware types.

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

I've been building calendaring APIs for a decade. This is basically my thing.

64 Likes

What is your evaluation of the proposal?
-1 (because some details imho could be improved drastically)

Is the problem being addressed significant enough to warrant a change to Swift? Does this proposal fit well with the feel and direction of Swift?
Yes; butā€¦

Date
As others already said: Let's not move a confusing name into the core of the language ā€” it's better to fix it.
Also, I think the conversion to NSDate and the loss of precision during encoding are unfortunate, and could be avoided with a new type.

Duration
The description for Date says

UInt32 for nanoseconds normalized where the nanoseconds storage will be no more than 1,000,000,000 nanoseconds (which is 29 bits)

But Duration is supposed to use Int64 to store nanoseconds. That makes me wonder: When 29 bits are enough to reach a full second, why use that much memory?
Also, it has been mentioned that we might need more granularity than nanoseconds. Therefore, I would at least move away from that unit (even with Date, we could easily increase precision by factor four by using all available bits).
As comparing Durations coming from different clocks is discouraged, and Clock already has a minimum resolution, I'd go a small step further and strip all units from Duration, so that it's just a big integer (and Clock defines how many units make up one second).

WallClock
I still think we need neither Date nor WallClock in the stdlib ā€” but if it's added, I don't think the name is good:
It has a measure function, but because of possible jumps, this seems to be a bad choice most of the time. Imagine someone receives the task to measure wall-clock time of some function, and has the choice to use either WallClock or MonotonicClock. Wouldn't it feel natural to measure wall time with WallClock?
But doing so will lead to strange results, because WallClock can change any time; you could even end up with negative duration!

Static variables
There has been a big improvement towards testability by moving to a model where everything is static to now being a property of each instance of Clock.
However, there are still some static declarations, and I find it hard to understand the reasoning behind those. Probably it's easier without having seen the first draft of the pitch, but afaics, encouraging the use of Instant.now is problematic:
Imagine you have a clock which is supposed to always return a fixed Date. This would be quite reasonable to mock WallClock in tests, but this will break silently when the .now-shortcut is used ā€” because your reference point suddenly looses all connection to your custom clock.
I think durations are more convenient than deadlines in most situations, but there are simple alternatives that neither compromise testability nor ease of use.

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
Read all posts in the pitch, did some research on the terms in question

13 Likes

In principle, I'm absolutely in favour of the core ideas in this proposal. However, and there I'm completely agreeing to everything already said in this review, at the current stage of the proposal, I am still -1.
I think that this proposal moved into review phase too quickly. Yes, the pitch thread was quiet for some time now, but that would have been the perfect moment for the author to address all the issues raised with the first version of the proposal and start a second pitch phase, exploring further some details like the naming of Date, etc.

Definitely, working with time, duration and clocks has always been awkward and unintuitive in Swift, so I think that this proposal ā€“ provided my issues with it get resolved ā€“ would improve the situation significantly.

Yes, but again, I have some issues with specific details of this proposal, all of which have already been raised here, so I won't repeat them again.

I have used clock libraries in many different languages, but not very extensively. Nonetheless, I would say, that this proposal overall is a pretty good solution to this problem in comparison to how it's solved in most other languages.

I did read the proposal and followed the whole pitch thread closely, however ā€“ in hindsight you're always smarter ā€“ I should have chimed in with my take on the problems with Date and Duration in the pitch thread. I thought that all the points that I could have made were already brought up by others and assumed that there would definitely be a second pitch phase where these exact issues would be explored in a more focused way, but seemingly that wasn't the case.

12 Likes

@davedelong has pretty much covered everything I wanted to say and moreā€”Iā€™m also -1 for the reasons discussed.

The only thing Iā€™ll add is that between the pitch and the beginning of this review thread, I encountered yet another DST bug caused in part by an assumption that Date would handle calendrical concerns like DST ā€˜magicallyā€™. These are mistakes that experienced programmers make, enabled in large part by the fact that Dateā€˜s name plainly misrepresents its functionality.

Iā€™ll also explicitly +1 the suggestion that convenience methods for .hours and .minutes be removed.

15 Likes

Can you clarify why .hours and .minutes are problematic, but .seconds are not? If I understand correctly (and I'm very much not sure that I doā€¦), the problem is that 3 hours on a wall clock may not actually correspond to three actual hours of elapsed real-world time due to things like DST jumps? But if that's the problem, I don't see how removing .hours and .minutes makes things any better; users will just do myWallClock.now + .seconds(3 * 60 * 60) instead. If offsetting wallClock.now by a fixed amount is wrong, it's just as wrong with seconds as with minutes or hours.

I think maybe I'm just not understanding what users should be using in this case?

8 Likes

My opinion, and why I advocated for their removal in the pitch thread, is that we should want users to do .seconds(3 * 60 * 60), since that makes it clear that they want a duration of exactly 10,800 seconds, rather than "3 hours" which is ambiguous in the face of things such as DST jumps. Performing the multiplication explicitly in source is much clearer about intent and behavior. I suppose we could also rename .minutes and .hours to something like .standardMinutes and .standardHours to give a little nudge that these may not correspond to every intuitive meaning of "hours," but I think it's better just to have users write out the multiplication explicitly.

ETA: we could also have some convenience constants like .secondsPerMinute and .minutesPerHour that give a bit more meaning to the multiplication while still giving an indication that the calculation is not calendar-aware.

12 Likes

The main reason is that you cannot avoid seconds when talking about temporal durations, because seconds are the fundamental unit of all physical time measurement.

Yes, people can do multiplication of factors to get longer units, but I believe it's worthwhile to discourage that by omitting the convenience methods to do so. This also speaks to the larger problem that .hours(3) means different things depending on which clock you're using, suggesting strongly that Duration should be a Clock associated type.

ETA: For situations like these in my own code, I go so far as to add these methods, but then mark them as unavailable so I can explain why they're the wrong thing to use:

extension Duration {
    @available(*, unavailable, message: "Use clock-specific APIs for creating durations that are measured in units longer than seconds")
    static func minutes(_ duration: Double) -> Self { ... }
    // etc
}

This is also why my Time library explicitly names this type as the SISecond.

If you'd like to discuss this more, I think that'd be better served for the pitch thread, so we can keep this one focused on reviews.

14 Likes

-1. I agree that it seems rushed.

  1. I still don't like the name InstantProtocol. It is a value, and you can advance it by a Duration and find the Duration between two values. It is our model of a point in time, or "moment" (as others have also pointed out).

    I feel that slapping -Protocol on the ends of names is a bad habit, that we should get out of. Generics are all about semantics and capabilities-based programming, and they work best when their abstractions are named appropriately for the high-level concepts they represent.

    Let's not continue the trend of bad naming in time APIs.

  2. The Date API is very barebones:

    @available(macOS 10.9, iOS 7.0, tvOS 9.0, watchOS 2.0, macCatalyst 13.0, *)
    @_originallyDefinedIn(module: "Foundation", macOS /*TBD*/, iOS /*TBD*/, tvOS /*TBD*/, watchOS   /*TBD*/, macCatalyst /*TBD*/)
    public struct Date {
      public init(converting monotonicInstant: MonotonicClock.Instant)
      public init(converting uptimeInstant: UptimeClock.Instant)
     
      @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
      public static var now : Date { get }
    }
    
    extension Date: InstantProtocol {
      public func advanced(by duration: Duration) -> Date
      public func duration(to other: Date) -> Duration
    }
    
    extension Date: Codable { }
    extension Date: Hashable { }
    extension Date: Equatable { }
    

    A Date is supposed to be a duration from a reference point (or "epoch"), but we don't have any way to get the Date of the epoch or to get the Duration from any commonly-used epoch, so there's no way for code to know what a Date "means". They're just opaque values. Which is really poor.

    Also, what about CustomStringConvertible? What is the output if I log a Date or use it in a string interpolation? Can it even be LosslessStringConvertible?

    (Also, Sendable, of course)

    EDIT: Oh, and Comparable.

  3. Readers may have noticed that Date remains Codable at the standard library layer but gains a new storage mechanism. The coding format will remain the same. Since that represents a serialization mechanism that is written to disk and is therefore permanent for document formats. We do not intend for Date to break existing document formats and all current serialization will both emit and decode as it would for double values relative to Jan 1 2001 UTC as well as the DateEncodingStrategy for JSONSerialization. This does mean that when encoding and decoding Date values it may loose small portions of precision, however this is acceptable losses since any format stored as such inherently takes some amount of time to either transmit or write to disk; any sub-second (near nanosecond) precision that may be lost will be vastly out weighed from the write and read times.

    I think this needs to be explained some more. When I read the words, it seems to be saying "encoding and decoding a Date may return a different Date", which is alarming, but then it has this bit at the end about how many nanoseconds it takes to write a file to disk? What is this even talking about?

  4. In addition to the per clock definitions of measuring a base measurement function using the monotonic clock will also be added.

    We tend to refer to these as "global functions". I think it could be made clearer that this proposal includes a new global measure function.

  5. I still don't like the name "wall clock". My computer doesn't have walls, and no wall clock I've ever seen behaves like WallClock. It also looks totally weird at the call-site:

    DispatchQueue.main.asyncAfter(deadline: .now.advanced(by: .seconds(3), clock: .wall)
    

    I continue to prefer "system clock" and clock: .system. We could potentially add the same .system static-member syntax for RandomNumberGenerator as well.

Also, I echo the concerns from above about the naming of Date. The Swift community clearly doesn't want or like a time API which uses Date to mean a non-calendrical instant in time. It's one thing to hear concerns, but it's another to listen to them. The Time API should be designed as though it were being made for Swift, to support existing and new technologies - both within Foundation's scope, and beyond it.

I wonder if a preview package might be a good idea? Clearly there are lots of people interested in the API, and it might be nice to test-drive it in order to gather better feedback.

22 Likes

Date is required to be both Sendable and Comparable by the adherence to InstantProtocol.

3 Likes

I agree with the first part (and am somewhat puzzled that there has been no reaction to the link givenā€¦ is Wikipedia considered to be wrong? Or irrelevant for Swift?), but although I think "system clock" is slightly better, it has a flaw as well:
System time is often identified as the time as it is displayed to a user of that system ā€” so it would return different values depending on the time zone the user has configured.
Date.now on the other hand should return the same value, no matter if you are in Austria or Australia. Therefore, I'd prefer something like UTCClock, CoordinatedClock, UniversalClock or WorldClock.

If "wall clock" is a term of art, and this proposal uses "wall clock" with a meaning that matches the expectations of a knowledgeable reader, then I'm fine with "wall clock". I see little value in being imaginative here. Reusing terms that have a different meaning in the litterature (I'm not saying you are doing this) would be even worse. This topic is already awfully complicated: there's no need to add our grain of salt

If the proposal suggest "wall clock" for what is commonly called, in computer science, a "wall clock", then we do not need to bikeshed it.

4 Likes

Apart from the criticisms of the naming and design of Date, which I generally agree with, the proposal has editorial issues that make it hard to understand what it means, particular in the Definitions section.

Absolute Time: Time that always increments, but suspends while the machine is asleep. The reference point at which this starts is relative to the boot time of the machine so no-two machines would be expected to have the same uptime values.

This is presented as if it was some sort of universal or neutral terminology, but as far as I know itā€™s Mach-specific ā€“ which is fortunate, because itā€™s an absurdly bad name. If ā€œabsolute timeā€ meant anything, it would be a time scale thatā€™s exactly the same everywhere, but no such thing exists, as famously proven by Einstein.

Luckily, this definition is only referenced in the definition of Darwin/BSD uptime, and can be removed.

Monotonic Time: Darwin and BSD define this as continuous time. Linux, however, defines this as a time that always increments, but does stop incrementing while the system is asleep.
...
Uptime: Darwin and BSD define this as absolute time. Linux, however, defines this as time that does not suspend while asleep but is relative to the boot.

Firstly, monotonic time has a clear and specific meaning: itā€™s time measured by a clock that can only advance, never regress. Whether it pauses when the machine is asleep is an orthogonal property.

Secondly, if we untangle these definitions it seems that the distinction between ā€œuptimeā€ and ā€œmonotonic timeā€ on Linux is exactly the opposite of what it is on Darwin and BSD. The proposal then goes on to define MonotonicClock and UptimeClock in alignment with the Darwin/BSD definitions, but without explicitly referring to the definitions or motivating this choice.

Naming these clocks, with their subtle distinction, based on the usage of a subset of platforms, where swapping them around would make just as much sense, seems like a poor choice. If we want these clocks to have precisely defined meanings, that should be reflected in meaningful names (like ContinuousMonotonicClock and SuspendingMonotonicClock). If we have a clock named UptimeClock, it should follow the platform convention for uptime, but such a thing should probably be in a platform-specific module rather than the Swift module.


Further down, we have:

They are distinctly NOT Numeric due to the aforementioned issue with regards to multiplying two TimeInterval variables. That being said, there is utility for ad-hoc division and multiplication to calculate back-offs.

There is no aforementioned issue; these two sentences are the only mention of multiplication in the prose. (That said, I agree that multiplication and division should only be allowed with scalars.)

no more than 1,000,000,000 nanoseconds (which is 29 bits)

30 bits (well, 29.897 or so).

Under Alternatives Considered:

It has been considered to leave the Duration type to be a structure and shared among all clocks.

Since this is the direction taken in the proposal, it shouldnā€™t be in Alternatives Considered.


How is Date.init(converting uptimeInstant: UptimeClock.Instant) intended to be defined? As far as I can see, implementing it accurately would require a log of all the times the machine has been suspended for as long as the process (or any Swift process on the same machine that it might RPC to) has been running.

17 Likes

I agree with most of the concerns raised above, but on another matter:

I thought of other clocks which, if added, would be best added via the stdlib: ProcessClock, TaskClock and possibly ThreadClock. These would be (mathematically) monotonic clocks which only and regularly tick while, respectfully, the current (or a particular other?) process, task or thread is running.

A possible use for these would be benchmarking: if a user stops and then resumes the test runner in the middle of a benchmark, I would consider it reasonable for the time while stopped to not be included in the benchmark's running time, which none of the currently proposed clocks would permit.

3 Likes

My general sense is encoding a value-type value and then decoding the external representation should be equality-preserving, because it makes it easier to reason about the program. For example, maybe I have a big, deeply nested structure that conforms to Equatable and Codable. I can test its round-tripability using plain old XCTAssertEqual. Then I add a Date property at some deeply-nested path in the structure. Suddenly my test starts failing, and I have to jump through hoops to fix it.

(Yes, NaNs destroy equality. But under this proposal, Date is not a floating-point type. We shouldn't be adding more special cases that make it harder to understand how our programs behave.)

If Date is entering the standard library anyway, perhaps we could teach the codable containers about it. For example, add an encode(_ value: Date) requirement to SingleValueEncodingContainer with a default implementation that, for backward compatibility, uses the lossy Double encoding.

Under this design, it becomes possible to extend some codecs to support lossless round trips in a backward-compatible way.

In particular, JSON doesn't limit the number of digits before or after the decimal point. So JSONEncoder and JSONDecoder can be taught (with some work on the underlying NSJSONSerialization) to encode and decode Dates as numbers with perfect accuracy, while maintaining compatibility with older implementations (and with non-Swift JSON codecs).

NSDate is already first-class in property lists, so perhaps the property list codecs could also be enhanced to preserve equality.

8 Likes

-1 I'm also not in favor of this proposal due to Date naming. As an API designer, I take as a guideline to always design for the future at the mercy of current users. If one does not believe that the number of future users will overtake the number of current users, then why the effort? By the same token, as a current Swift user, I'm more than happy to carry the burden so others don't have to in the future.

12 Likes

I have a lot of sympathy for feedback already given.

The proposal speaks to impacts on existing concurrency APIs. It seems I can pass a clock and deadline so my tests donā€™t wait for the deadline literally. If I understand this right, this is a great addition.

However, how will we model these types so we can pass different time passage behaviour into it to test them? Will we have to write and use yet another type erased wrapper AnyClock everywhere? Were alternatives to a clock protocol considered?

I wonder how this proposal might impact future concurrency APIs, too. For example, debounce, throttle and delay streams. Presumably these obviously missing APIs will quickly follow through and take clocks, durations, deadlines etc in such a way that developers can control the passage of time and test their async streams?

I appreciate the thought the authors have put into this proposal, and I appreciate the critiques that have already shown up in this thread. I enjoy using the strongly-typed Instant/Duration model very much in Rust (and Dispatch), and I can look forward to its eventual inclusion in Swift.

Iā€™ll put in one comment I havenā€™t seen here: several people have commented about Duration.nanoseconds having more precision than needed, but even if itā€™s reduced to Int32, are there constraints on its representation? For example, will nanoseconds ever have a different sign from seconds? Will it ever exceed 1 billion? These are general questions about normalization, but itā€™s important for anyone trying to access the fields.

5 Likes

Would it be a problem to use full width of the field as an unsigned fraction? So instead of counting 10^-9 seconds it would count 2^-32 seconds and there'd be no need for constraints or normalisation. Nanoseconds would be easily obtainable from this fractional unit.

1 Like