SE-0329 (Third review): Clock, Instant, and Duration

Hello, Swift community.

The third review of SE-0329: Clock, Instant, Duration begins now and runs through January 24th, 2022.

Like the first review, the second review of this proposal produced a lot of feedback from the community. The Core Team has gone over this feedback and come to several conclusions:

  • There is much broader satisfaction with the current proposal in the community. A lot of people were opposed to the first proposal, but people generally approve of this one. That said, the community does have some specific concerns about some aspects of the proposal, which I'll address below.

  • Concerns were raised about the tolerance parameter to the sleep methods:

    • Programmers can pass .none, which will be interpreted as a nil optional and thus a request to use the default tolerance, and that this can be easily misunderstood by readers as a request for zero tolerance. The Core Team acknowledges this problem; however, we feel that it's a much wider issue than just this method, and we don't want to create a precedent of avoiding Optional when it's otherwise the right design. We should instead consider ways to address this problem more directly rather than working around it, perhaps by improving autocompletion to not suggest .none or even deprecating the use of .none when nil would be allowed. The Core Team invites discussion and proposals on this issue.

    • The default tolerance is abstract (nil), rather than something you can query (e.g. a static defaultTolerance property). The Core Team believes that this is a natural consequence of a reasonable underlying model, and we think that keeping the default tolerance abstract is the best approach for the language. Schedulers need to be free to use their own, evolving heuristics for tolerance, taking into account a wide variety of possible inputs; schedulers on Apple platforms already do this, and it's a reasonable evolution for any scheduler.

  • It was observed that the preposition for: in sleep(for:) could be omitted as a needless word. However, there are good reasons to keep it: most importantly, that this is one method in an overload set where the alternatives (sleep(until:)) use a contrasting preposition. The Core Team agrees that for: should remain.

  • The measure requirement is not currently implemented because to implement it as proposed requires the reasync feature, which is currently just a future direction. The Core Team suggested that it might be best to turn measure into an extension method (rather than a protocol requirement) and implement it in a way that avoids ABI constraints, so that in the future we can simply replace it with reasync. The authors agreed that it does not need to be a protocol requirement, and they have revised the proposal accordingly.

  • There was discussion about the precision of the concrete Duration type. The authors have clarified that the type is designed to be a 128-bit count of attoseconds, and that this is not reflected in the API simply because Swift lacks a portable 128-bit integer type. The Core Team is satisfied with this, pending future directions to expose the full precision.

  • There were several concerns about the .seconds and .nanoseconds instance properties. First, these names also exist as the base names of some static methods, which can cause conflicts with abstract references to the properties (e.g. as key paths). More importantly, the .nanoseconds property may be confusing because it actually just returns the sub-second nanoseconds, as is useful for e.g. the C timespec type; thus e.g. Duration(nanoseconds: x).nanoseconds does not necessarily round-trip. The authors propose renaming these to .secondsPortion and .nanosecondsPortion, which resolves both problems.

  • Several members of the community expressed a concern that this proposal no longer includes provisions for getting the current system time. System time is intricately bound up with questions of human timekeeping and calendrical systems. This proposal is focused on the much narrower problem of timekeeping for scheduling and does not preclude addressing system time and/or calendars in the future. The Core Team believes that this proposal is useful as it stands.

The authors have already made their revisions to the proposal, and we are putting it immediately back into review. The review is restricted to the changes around .secondsPortion and .nanosecondsPortion; all other aspects of the proposal are accepted.

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?
  • 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

21 Likes

If the Foundation.TimeInterval (aka Double) conforms to DurationProtocol, will the operator functions taking rhs: Int cause any issues?

public protocol DurationProtocol: Comparable, AdditiveArithmetic, Sendable {
  static func / (_ lhs: Self, _ rhs: Int) -> Self
  static func /= (_ lhs: inout Self, _ rhs: Int)
  static func * (_ lhs: Self, _ rhs: Int) -> Self
  static func *= (_ lhs: inout Self, _ rhs: Int)
  
  static func / (_ lhs: Self, _ rhs: Self) -> Double
}

On the secondsPortion and nanosecondsPortion APIs, I still think it might be useful to also get milliseconds and microseconds — which would otherwise require error-prone conversions by each client.

For example, I fixed a bug in swift-corelibs-foundation which was multiplying by 1.0e9 instead of 1.0e6 (so the fractional portion was unexpectedly 500 seconds rather than 0.5 seconds).

I certainly hope we're not talking about adding heterogeneous operators to Double through a backdoor.

nanosecondsPortion is specifically the sub-second nanoseconds; it sounds like that use case wants a total number of milliseconds. Maybe there's a naming problem here.

wholeSeconds and partialSecondsInNanoseconds?

But that doesn’t sound like it would have avoided the bug @benrimmington fixed, which was simply choosing the wrong constant to multiply by.

The use case is the same, but I don't know if my suggestion would have helped.

FileManager was converting from TimeInterval (aka Double) to a POSIX timeval.

  • First, it used modf to get the integral and fractional parts.
  • Then it used 1.0e9 * fractional for the timeval.tv_usec microseconds field.

On Raspberry Pi OS, a value greater than 999_999 was rejected (with EINVAL errno).

On other platforms, the value was accepted — similar to the Duration.microseconds(_:) static method — but the result was incorrect (too far forward by up to 1000 seconds).

In this case, the naming problem might be in the POSIX APIs.

Just on the naming: I feel that nanosecondsComponent and secondsComponent is more in keeping with existing terminology in Swift (I'm not aware of any "portion" methods), and that alternatively nanosecondsPart is both shorter and (in my opinion) more obvious than nanosecondsPortion.

12 Likes

TimeInterval cannot conform to DuationProtocol for this reason. I feel like most maintainers will be transitioning away (likely progressively) from TimeInterval to the more strongly typed Duration.

Date for example will be conforming to InstantProtocol and the Duration will be defined as Swift.Duration per the Foundation part of the proposal.

5 Likes

Component suffixes seem like a reasonable option that I would be open to. It fits with the Foundation APIs of DateComponents and URLComponents. So I appreciate the symmetry.

There is another alternative: to have the two properties be one singular property that is a tuple of the two parts. That property could be named components and the tuple then could have named fields seconds and nanoseconds.

8 Likes

Given the existence of both struct timeval and struct timespec, I’m revising my previous suggestion:

public enum FractionalSecondsScale {
  case milliseconds // useful for humans
  case microseconds // useful for populating `struct timeval`
  case nanoseconds // useful for populating `struct timespec`
  case attoseconds // should we choose to expose it
}

public struct Duration: Sendable {
  public var wholeSeconds: Int64 { get }
  public func fractionalSeconds(as: FractionalSecondsScale) -> Int64
}
1 Like

That feels like the better answer is just to expose an Int128 attoseconds property (and nothing else). But that isn’t an option currently.

The goal is to save the client from writing any conversions themselves, because it’s pretty easy to mix up the conversion factors between various derived SI units.

import Darwin

// Sets a file’s `mtime` and `atime` to the current time. See `man 1 touch`.
func touch(path: String) {
  let now = Clock.continuous.now
  var tv = timeval()
  tv.tv_sec = now.wholeSeconds
  tv.tv_usec = now.fractionalSeconds(as: .microseconds) // look, no math!

  let times = [tv, tv]
  path.withCString {
    utimes($0, &times)
  }
}
2 Likes

The modf API returns both components with the same sign:

modf(-Double.pi)  //-> (-3, -0.141_592_653_589_793)

Should a negative duration return two negative components?

let duration = Duration.seconds(-Double.pi)
duration.components  //-> (seconds: -3, nanoseconds: -141_592_653)

If the API only returns nanoseconds, it can't support picoseconds for benchmarking.

Thanks @Torust - I also like Component suffixes, nice symmetry. Would argue against tuple as that does not scale very well if you in the future want to add other Components.

1 Like

Being the original suggester of the tuple idea, I am very much in favor of it. I also like the idea of calling it components. @ksluder's insight about a major use case helpfully leads to the insight that users are unlikely to need just the sub-second portion alone without also accessing the whole seconds. This strengthens the argument for one API that returns both components.

I have a further suggestion building on that which--I think--addresses all of these points above about extensibility, perhaps even to attoseconds eventually:

extension Duration {
  enum Unit {
    case seconds, milliseconds, microseconds, nanoseconds
    /* extensible, so attoseconds can be exposed later;
       these should parallel the static functions on `Duration` */
  }
  func components(
    _ firstUnit: Duration.Unit, _ secondUnit: Duration.Unit
  ) -> (Int64, Int64) { ... }
}

This design offers maximal flexibility, allowing one API call to give users all the information needed to populate a timeval or timespec. Additionally, it also allows users to request the whole duration expressed in milliseconds or nanoseconds with the same API, should they wish--e.g., for calling Task.sleep(for: nanoseconds(...))--throwing away the second component.

(In the case where a user requests duration.components(.nanoseconds, .seconds), the API can decide always to provide the fractional part for the smaller unit or just trap; in the case where, say, a user requests a whole duration in nanoseconds that cannot be represented in Int64 the API probably should trap.)

3 Likes

Would "quotient" and "remainder" make sense in your suggested API?

extension Duration {
  public func quotientAndRemainder(
    _ quotientUnit: Unit,
    _ remainderUnit: Unit
  ) -> (quotient: Int64, remainder: Int64)
}

Let’s keep in mind that programmers who are interacting with this API are already thinking in terms of “seconds” and “fractions of a second”. “Quotient” and “remainder” describe how to calculate those values, but that’s one layer of abstraction removed from the terminology the programmer is actively working with and the operation they are trying to perform.

Likewise, the flexibility afforded by taking an arbitrary, unordered pair of units seems to bring along quite a bit of potential confusion. What do we lose by constraining the API to specifically offering whole and fractional seconds (whether as a tuple or as separate properties)? I ask because I certainly think we gain a lot of clarity at the call site.

5 Likes

This is indeed an important use case. As I alluded to above, I think using the same method to return both whole-and-partial seconds and whole subseconds is more confusing than helpful. I’d advocate keeping the methods separate. Using a tuple:

public struct Duration: Sendable {
  /// - Returns: A tuple of whole and fractional seconds. The scale of fractional seconds is specified by the `fractionalScale` argument.
  public func components(with fractionalScale: FractionalSecondsScale) -> (Int64, Int64)

  /// - Returns: The number of fractional seconds encompassed by this Duration, rounded toward zero. The scale of the fractional seconds is specified by the `fractionalScale` argument. If the value does not fit into an Int64, this function returns nil.
  public func wholeNumber(of fractionalScale: FractionalSecondsScale) -> Int64? 
}

Though I still prefer separate wholeSeconds and fractionalSeconds members.

1 Like

I very much like this approach, and (so far) I'm most in favor of this.

While it's true that you almost never need fractional seconds without also needing whole seconds, I think this makes the most sense for how the unit actually gets used. Your point about not wanting callers to do the division themselves is also spot-on.

I agree with the sentiment that we almost always need whole and fractional seconds together, but I think this approach offers too many ways for things to be unclear. You alluded to the duration.components(.nanoseconds, .seconds) being potentially problematic, but there's also the issue of "what if they don't ask for seconds at all?", ie duration.components(.milliseconds, .nanoseconds). Trapping (as suggested) seems unnecessary when there are alternate approaches that cannot be misused.

Perhaps the "best of both worlds" approach could be something like this:

extension Duration {
    public enum FractionalSecondsScale {
        case milliseconds // useful for humans
        case microseconds // useful for populating `struct timeval`
        case nanoseconds  // useful for populating `struct timespec`
        case attoseconds  // should we choose to expose it
    }

    public func components(with scale: FractionalSecondsScale) -> (wholeSeconds: Int64, fractionalSeconds: Int64)

    // conveniences around the `components` method for the cases
    // where the caller needs to deal with the values separately
    // so the caller can use these inline without worrying about destructuring a tuple
    public var wholeSeconds: Int64 { get } 
    public func fractionalSeconds(`as` scale: FractionalSecondsScale) -> Int64
}
2 Likes

This is the use case I allude to above about asking for the whole duration in, say, microseconds for the Task.sleep(for:) API. This isn’t misuse: enabling that use case without requiring the user to do their own full-width multiplication is one of the reasons why I suggested this design. The alternative is, as you show, not to permit users to get the whole duration in microseconds at all, but I would like to expose that functionality.

Yep, I agree. @ksluder's suggestion of another method to get the whole value in a particular scale is what I would go with for that.