SE-0329 (Second Review): Clock, Instant, and Duration

It’s not known to me—which influential libraries, and for what reasons?

I only know that certain Swift-affiliated libraries avoid Foundation because they’re meant to incubate possible future additions to the standard library, which would then cause a layering violation if it required Foundation. That is inapplicable as a general consideration for users though.

3 Likes

I'm aware of SwiftNIO. It uses its own ByteBuffer instead of Data and I can't find anywhere it imports Foundation except in a separate compatibility module and in some of the tests.
And swift-nio-http2 has its own separate copy of Foundation's Base64 decoding, presumably to avoid importing Foundation. So to me it seems to be a policy to not include Foundation, but I don't know the reason.

I've also stumbled on Perfect (web server toolkit) that does not appear to use Foundation anywhere. It has no support for Data (using [UInt8] everywhere instead), which I found annoying. Again, not sure of the reasons, but perhaps Foundation on Linux wasn't good enough when this project started.

I don't know if that lives to the standard of "some influential libraries". My sample size is rather small. All I know is that doing some server-side stuff recently left me me with the impression that server-oriented projects are avoiding Foundation.

4 Likes

Without going too deep on the tangent of Foundation here, I can just provide one more data point as someone who is just about to start a fairly sizeable many-person, multi-year server side swift project - we’ll avoid depending on Foundation completely and actively work around depending on it. (This is based on experience from using both Foundation and CF on multiple platforms)

Anyway, to focus on the topic at hand:
To have currency types in either the standard library or in e.g. swift-system (or future similar endeavors - I’d personally be ok with a new swift-time library or similar) is preferred.

I can just echo that the latest proposal looks a lot better, but the baby might have been thrown out with the bath water a little bit when Date wasn’t sunk into the standard library due to the discussion on alternate names. If the alternative is that we will need a Foundation dependency I’d rather live with the non-perfect name…

6 Likes

To me at least this is becoming a more and more central part of this review and not at all a tangent. Here is the official text on what belongs in the standard library versus Foundation:

We believe that the Swift standard library should remain small and laser-focused on providing support for language primitives. The Foundation framework has the flexibility to include higher-level concepts and to build on top of the standard library […]

In general, the dividing line should be drawn in overlapping area of what people consider the language and what people consider to be a library feature.

I don’t think anyone is pushing back on the basic observation in this version of the proposal that certain time-related APIs such as those required to support Swift’s concurrency features (e.g. sleeping a Task until a deadline) are more “primitive” in supporting the language itself as compared to the Date-related functionality that’s now in Foundation: indeed, you (and I) have stated that another first-party library like swift-system would be a fine place for such APIs. The currently proposed division of what features go where is incontrovertibly the more “correct” arrangement by those criteria.

What I understand to be @Karl’s position and what you’re stating, is one or both of two things: disagreement with the overall idea of and criteria for splitting core functionality between the standard library and other core libraries, and/or objection that Foundation is unfit for purpose as the major core library on the other side of that split. This is a major and pressing problem here if, as you say, folks would be willing to accept design decisions for the standard library that the community overwhelmingly rejected specifically to avoid using Foundation for its stated purpose.

This would be like if we were discussing whether to make a blueberry pie or an apple pie, and then whether we should share the pie with Alice or Bob—but Alice has a blueberry allergy and it turns out our apples are rotten. The logical conclusion, then, would be that we should make a blueberry pie and invite Bob. Apparently, though, there’s a sizable contingent of people who would rather eat rotten apples with anyone else than share something with Bob, and no one wants to talk about why they’re avoiding Bob. This is kind of a big deal if true and needs to be investigated, because the alternative is we eat a bunch of rotten apple pies. At face value we’d say it’s because we carefully weighed the pros and cons of blueberry versus apple pie, but in fact it’d be because we’re avoiding Bob.

6 Likes

This would be my preferred path. Time is an incredibly important concept for most programs, and this seems to me like it strikes a happy medium between accessibility and not having to import Foundation. What's more, this model of small-ish highly specialized semantically versioned libraries is working remarkably well for swift-collections and swift-algorithms, with high adoption and fast iteration.

An open-source swift-time library would enable us to explore the problem space as a community, and also give us the opportunity to graduate some types to the Standard Library if after some experience it is deemed necessary.

11 Likes

Thanks @xwu, you are logically to the point (as usual) - I’ll take the (pie-)bait and clarify the rationale from my point of view, but perhaps we should move the discussion elsewhere if we believe it can be fruitful.

Definitely agree.

You are absolutely right of course. I have no disagreement with the overall idea of keeping the standard library laser-focused on providing suppor for language primitives, I think that is a fundamentally very sound design decision.

I am as you suggest in the other camp where I think that Foundation is not fit for purpose for the other side of the split, specifically when developing server-side Swift services which has no requirement to integrate with Apple frameworks by default. With server-side I suggest services running both on Linux as well as on macOS but without requirements to integrate with proprietary Apple frameworks.

I fully understand the original decision to support Foundation to facilitate integration with Apple platforms and frameworks in a reasonable way, but it comes burdened with a long legacy in terms of API and functionality (quarter of a century or so at this point… When we first started using Foundation in the mid-90s it required installation of runtime libraries...) and is fairly big in scope - and I don't want to disparage the effort put into it and understand its valuable for many use cases, but when starting development of something brand new on the server, I'd rather not be tied to that legacy (also related is that there is not feature-parity on Linux/macOS there).

That being said, I’d personally rather go the route as suggested by @nikitamounier (and possibly yourself?) and others and adopt a model of smaller specialized semantically versioned libraries, where we have not only swift-collections and swift-algorithms, but also swift-nio, swift-log, swift-service-lifecycle, swift-argument-parser and swift-system, ...

I think the evolution of all of these smaller (good!) Swift packages instead of pushing all of this into Foundation (to which there are also an overlap of functionality) says a fair bit.

I believe that would allow for faster pace of development and not being constrained by compatibility requirements as well as allowing for "doing the right thing" for the long term (which I believe and hope would be Swift being a strong contender on multiple platforms, hopefully supplanting C++/Java/Rust as default choices on the server side).

I think that instead choosing to go for a new swift-time library would make a lot of sense in the same vein and could see a future where the Foundation compatibility library would possibly be built and use such "core" Swift libraries.

E.g. it'd make more sense to have reference collection types in swift-collections and possibly later in the standard library than having them live in Foundation with legacy NSxxx namespacing.

Really it means switching the view of Foundation as a core Swift library to become more of a compatibility story and instead having this set of smaller, focused and unburdened libraries to flourish.

Just my 0.02c - apologies if this is out of scope, would be happy to split this discussion out in that case - but that'd be one more concrete input into the review itself - please consider going the route of swift-time.

8 Likes

Fully agree. Swift is still quite a young language, so it's really important we get time right the first time so as to not fracture Swift 20 years from now once it has reached global domination (I say this half-jokingly).

IMO we cannot in good faith push for ownership control / performance annotations, which are in part aimed at expanding Swift's use in embedded systems, while forcing users to import a big monolithic >10mb library just to get the current date or get the time in Paris.

I understand that one could say this about just about anything in Foundation – for instance JSON en/decoding is essential for embedded systems using JSON-RPC – but I do sincerely think that Time is just that important. Much like @hassila above, I don't mean to belittle / depreciate the effort that has been put into Foundation, nor underestimate the role it has played in Swift's continued growth (it's played a huge role), but I don't think it's right for the role of Time in the long-run.

That's why I think swift-time would be perfectly suited for the job – its cousins have showed that these kinds of libraries do enjoy high adoption and quick yet meaningful iteration, and seems to me to be much better placed for a future Swift where importing Foundation isn't the absolute default.

Furthermore, taking a more present perspective, today swift-time would mean that all iOS/macOS developers will be able to enjoy these new, more modern and potentially more correct APIs for any OS version they target, not just the latest one – and idem for when APIs are changed / added under the semantic versioning rules.

1 Like

If there's real need for the "new Date" currency type, it can start its life outside of standard library like Result, then mature and later be incorporated into standard library. Might well be the case that the hour has not yet come for this new currency type, and most projects use Double or struct timeval and very few projects need dates even beyond 2038 with a second precision, let alone some 20x age of universe future dates with attosecond precision.

Result is a pretty bad example here. Though I'm not privy to why the core team decided Result was worth adding (seems to be partially a hedge ahead of ABI stability), I drove the proposal because it was getting more and more painful for the community to deal with multiple representations of the same type. Allowing "the community" to vet potential additions to the standard library just doesn't work and, despite early talk of the evolution process being driven by it, it has yet to fully drive an addition to the language. Result is about as close as we get, but it was only enabled by last minute Error self-conformance heroics which no other proposal would receive.

Additionally, the complexity of proper time and calendaring APIs means that we'll see few, if any implementations that don't rely on some part of Foundation. Why would anyone spend that time and effort with no way to guarantee compatibility with the APIs people already use, or whether they'd see any adoption at all?

So in the end, I don't think "see what the community comes up with" is a viable or healthy approach for Swift to take, especially for something like time representations.

2 Likes

I'm very happy with the revisions, and the updates to clarify that calendrical calculations aren't going into the standard library. While I wasn't as dismayed about the use of Date, I think this resolution will lead to reduced confusion long-term.

Absolutely, yes. It's been a critical piece missing so that the standard library can reason about and compute related to durations, and critical for the concurrency features that also hugely benefit the language.

I'm not as familiar as others with other implementations, mostly being a consumer of the end result, so I can't really opine on this point.

In depth reading and following of from the original pitch through the review process, as well as detailed study of the interactions with other evolution components and prior API art within Swift (aka Combine).

As a follow-on note, I know a number of folks are (quite reasonably) wishing that we had amazing great calendrical APIs in the standard library, but I much prefer to see the focus on the critical aspects here, with an ability in the future to it in extended swift use cases such as embedded/systems development, forgoing the massive work that calendrical computations would require. I think it's by far the better trade-off.

3 Likes
public protocol Clock: Sendable {
  /* ... */

  var minResolution: Instant.Duration { get }

  func measure(
    _ work: () async throws -> Void
  ) reasync rethrows -> Duration
}

The measure method is a protocol requirement in the proposal, but not in the v2 implementation.

The return type should probably be Instant.Duration.


The clock minimum resolution will have a default implementation that returns .nanosecond(1).

This isn't possible for all clocks — if their Instant.Duration isn't Swift.Duration.

Could the minResolution property return nil by default?


public struct Duration: Sendable {
  public var seconds: Int64 { get }
  public var nanoseconds: Int64 { get }
}

Can we also have instance properties for milliseconds and microseconds? This would be useful when converting to another type.

Should the instance properties be named differently from the static methods?

let x = Duration.nanoseconds(Int64.max)
x.nanoseconds == .max  //-> false
x.nanoseconds          //-> 999_999_999

let y = Duration.seconds(Double.pi)
y.seconds  //-> 3

Mmm, this is a very good point. It took me a second to begin to understand the example; let me see if I can rephrase the point correctly:

Duration.nanoseconds returns the fractional nanoseconds (that is, the total duration minus the number of whole seconds), whereas Duration.nanoseconds(_:) constructs a total duration given in nanoseconds. This is confusing when the two APIs are juxtaposed.

In the present design, there would also be confusion if convenience instance properties milliseconds and microseconds were added, since it would imply different semantics for the instance property nanoseconds (that is, does it return the total duration minus the number of whole microseconds, or still minus the number of whole seconds?).

Perhaps, instead, we could have a single instance property public var subdivisions: (seconds: Int64, nanoseconds: Int64) { get }.

4 Likes

I like your idea of returning a tuple, but a method might be better than a property.

extension Duration {

  public enum FractionalUnit: Int64 {
    case milliseconds = 1_000
    case microseconds = 1_000_000
    case nanoseconds  = 1_000_000_000
    case picoseconds  = 1_000_000_000_000
    case femtoseconds = 1_000_000_000_000_000
    case attoseconds  = 1_000_000_000_000_000_000
  }

  public func normalized(
    fractionalUnit: FractionalUnit
  ) -> (seconds: Int64, fractional: Int64)
}

For example:

let duration: Duration = .nanoseconds(42_123_456_789)
let tuple = duration.normalized(fractionalUnit: .microseconds)
tuple.seconds     //-> 42
tuple.fractional  //-> 123_456

There's still the issue that both elements must be signed, because negative zero isn't possible.


On the naming of Instant.Duration and Swift.Duration, would a different top-level type name be less confusing?

(AnyDuration or MetricDuration or SIDuration or …)

On second thought, two separate APIs could work, if the return types were changed:

extension Duration {
  public var seconds: Float64 { get }
  public func fractional(_ unit: FractionalUnit) -> UInt64
}

Can the conversion avoid rounding to the nearest whole number of seconds?

let duration: Duration = .nanoseconds(999_999_999_999_999_999)
duration.seconds                 //-> 999_999_999.999_999_900
duration.fractional(.nanoseconds)            //-> 999_999_999

The seconds are (1_000_000_000.0).nextDown in this example.

The Core Team has discussed the second review of SE-0329 with the authors, and a small number of changes have been made to the proposal. SE-0329 is now back in review for a third time, albeit only on a very small point; please see the new review thread for the Core Team's decision.

3 Likes