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 staticdefaultTolerance 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:
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.
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_usecmicroseconds 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.
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.
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.
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
}
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, ×)
}
}
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.
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.)
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.
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.
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
}
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.