@Philippe_Hausler Do you have the original pitch text up on GitHub somewhere so I can submit a bunch of suggestions for nitpicks of grammar and spelling etc.? I promise to keep all conceptual remarks in the forums! ![]()
not just yet, I will post that here shortly
I'm only seeing possible implementations of a manual clock based on global static state, thread local state, or async task state. Am I missing an option? In either case, would you mind sharing your implementation? I'm sure it would help the discussion to see how you envision using the API, especially with custom clocks.
OK, I'm back!
As promised, this is an adjustment to the API that moves now to be on ClockProtocol but still allows you to use .now().advanced(by: whatever) on DispatchQueue extensions. The solution is to use an intermediate type:
The setup:
public protocol ClockProtocol {
associatedtype Instant: InstantProtocol
var now: Instant { get }
// and maybe other methods
}
public protocol InstantProtocol: Comparable, Hashable {
func advanced(by duration: Duration) -> Self
func duration(to other: Self) -> Duration
}
extension InstantProtocol {
static func + (lhs: Self, rhs: Duration) -> Self { … }
static func - (lhs: Self, rhs: Duration) -> Self { … }
static func - (lhs: Self, rhs: Self) -> Duration { … }
}
public struct Duration: Sendable {
public var nanoseconds: Int64
}
Then we introduce:
public struct InstantSpecifier {
static func + (lhs: Self, rhs: Duration) -> Self { lhs.advanced(by: rhs) }
static func - (lhs: Self, rhs: Duration) -> Self { lhs.advanced(by: Duration(nanoseconds: -rhs.nanoseconds)) }
public static var now: InstantSpecifier { .init(offset: .zero) }
public var offset: Duration
private init(offset: Duration) {
self.offset = offset
}
public func advanced(by offset: Duration) -> Self {
return InstantSpecifier(offset: Duration(nanoseconds: self.offset.nanoseconds + offset.nanoseconds))
}
}
Finally, we change the extensions on DispatchQueue etc to use InstantSpecifier:
extension DispatchQueue {
func asyncAfter<C: ClockProtocol>(deadline: InstantSpecifier, clock: C, qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = [], execute work: @escaping () -> Void) {
let instant = clock.now + deadline.offset
// TODO: dispatch at the computed C.Instant value
}
}
extension RunLoop {
func run(until: InstantSpecifier) {
let targetTime = WallClock().now + until.offset
self.run(until: targetTime)
}
}
The usage is as expected:
DispatchQueue.main.asyncAfter(deadline: .now, clock: .wall) { print("Hello, wall") }
DispatchQueue.main.asyncAfter(deadline: .now.advanced(by: .seconds(3)), clock: .uptime) { print("Hello, uptime") }
DispatchQueue.main.asyncAfter(deadline: .now + .milliseconds(42), clock: .monotonic) { print("Hello, monotonic") }
RunLoop.current.run(until: .now + .seconds(10))
This actually seems more so of an actual Deadline type. Since that is how it would be intended to be used. However there is one problem w/ this that I am still grappling with: the call site of creation and the use site may have time elapsed between them. That means that the deadline will be artificially advanced.
Perhaps there should be also some way of constructing one of these deadlines given an Instant? Also it seems that type should be generic upon the base Instant type in that case.
Generally looks good but I haven’t gone through in details.
Talk of “exotic” clocks makes me think of some media formats that have fractional (numerator and denominator) time bases. I’m not sure whether for long running videos a conversion to a non fractional value can be kept accurate without drifting due to rounding errors.
I’m not offering a specific requirement or solution I’m afraid but just another thing to keep in mind.
Good luck with it.
That is definitely a use case I had in mind; thankfully the Instant type is rather opaque and so that means the implementor would be the one that would need to account for that. Which I would hope they are thinking about when doing so.
I think Double suffices for floating-point (Float80 isn't really used for this sort of thing, and Float32 and smaller generally lack precision to satisfactorily represent durations).
I do think that <T: BinaryInteger>(_ xxxx: T) inits would be perfectly reasonable, however.
The initial reason was to capture the zoo of duration types that are commonly used. But you are correct that a generic probably would be better.
Trying that out seems to work great. I will update that accordingly.
I'm very glad to see this topic tackled, thanks for opening this! ![]()
I was about to react to a couple of points in the proposal, including the fact that now should definitively be on Clock not on Instant, then I saw Dave's answer which actually perfectly captures my thoughts and the feedback I wanted to give (though he wrote it better than I'd have
).
So +10000 to everything in [Pitch] Clock, Instant, Date, and Duration - #29 by davedelong ![]()
I also strongly agree with this. It is actually of the main thing I find very annoying with the current Date/NSDate in Foundation (alongside the fact that NSDate should never have been called Date but Instant instead like in your proposal, to avoid common confusion with human concept of date, but I digress).
The fact that now is defined as a static on NSDate in Foundation gets in the way of unit testing as it is not injectable easily. In fact in practice I've often had to create my own Clock type in my projects as a provider for the now, in order to be able to inject my own MockClock in unit tests and be able to generate consistent Instants and simulate clock advancement with manual explicit calls in those tests.
This exact same idea can also be seen in RxSwift and it's Scheduler protocol (a bit similar in concept to the Clock from this proposal) and the TestScheduler type it provides which is so useful for testing.
Having .now with type inference as call site might seem nice and tempting at first but it assumes the concept of now is a global and shared one, making it a hidden static dependency and not allowing for custom clocks nor dependency injection for unit testing and/or debugging.
Being able to manipulate clocks is also vital for scientific applications and simulations.
For example, ROS (the "Robot Operating System") uses its own time abstractions for this:
When playing back logged data it is often very valuable to support accelerated, slowed, or stepped control over the progress of time. This control can allow you to get to a specific time and pause the system so that you can debug it in depth. It is possible to do this with a log of the sensor data, however if the sensor data is out of synchronization with the rest of the system it will break many algorithms.
Another important use case for using an abstracted time source is when you are running logged data against a simulated robot instead of a real robot. Depending on the simulation characteristics, the simulator may be able to run much faster than real time or it may need to run much slower. Running faster than real time can be valuable for high level testing as well allowing for repeated system tests. Slower than real time simulation is necessary for complicated systems where accuracy is more important than speed. Often the simulation is the limiting factor for the system and as such the simulator can be a time source for faster or slower playback. Additionally if the simulation is paused the system can also pause using the same mechanism.
IIRC, there were efforts to model this using a custom std::chrono clock, but it wasn't possible because of static now().
Of course, these specific applications may want their own time abstractions anyway, but the point is that controlling the clock can be a broadly useful thing. It would also be tremendously helpful if custom time abstractions could at least interoperate with standard library protocols such as ClockProtocol, and static members make that more difficult.
So to address the issue of a static now versus instance per clock now there are a few things that will need to be done. I have a potential approach that I should have some updates for y’all tomorrow; obviously it is a tricky balance to get right so I want to make sure the experts involved so far before this pitch get a chance to weigh in on my revision.
Would the T: FloatingPoint be useful for Decimal64 and Decimal128 types?
It would probably be more useful to add a T: DecimalFloatingPoint init once those exist, since it can generally be somewhat more efficient.
As promised here is some refinements:
First and foremost a branch for the proposal so far: [Pitch] Clock, Instant, Date, and Duration by phausler · Pull Request #1452 · apple/swift-evolution · GitHub
The changes incorporated so far from the initial post of the pitch
- Re-home now from
InstantProtocol.nowto the instance variablenowonClockProtocol - Re-home the
duration(from:to)fromClockProtocolto an instance method onInstantProtocol - Added a whole mess of operators for doing common manipulation like adding a Duration to an Instant, subtracting two Instants to get a duration, but also division and multiplication operations on Duration for interacting with fractional portions of Durations for implementing back-off algorithms.
- The
.nowfor shorthand was resolved by adding a static instance of now to the concrete clock types. So to get the proper shorthandsMonotonicClock.Instant.nowreturns theMonotonicClockinstance's now. - Added an example of a manual clock usable for testing (caveated that is not part of the proposal at this time)
- Changed the initializers for
Durationto takeBinaryIntegerinstead of relying on specific overloads - Removed
.hours(_:)and.minutes(_:)creation forDuration - Fixed some bad spelling
I'm liking the refinements, and thank you for including the manual clock as an example! ![]()
One question though. The implementations of WallClock, MonotonicClock, and UptimeClock all declare now as static var, but without a non-static equivalent to satisfy the protocol. I'm guessing the intent is that the static versions are intended for convenience, and the missing instance vars are an oversight?
The protocol requirements are inferred. I can add them back to be more clear; I just wanted to keep it super clear that there was replication.
I will alter the pitch document to clarify that.
One other bit of confusion for me — the detailed design shows Duration as a struct, but the "Impact on Existing APIs" and "Alternatives Considered" both read like they're from a version of the design where Duration was an associated type of the clock:
extension Task where Success == Never, Failure == Never {
public static func sleep(for duration: MonotonicClock.Duration) async throws
}
Good catch, that was leftovers from when the duration type was separate per clock. That should read:
extension Task where Success == Never, Failure == Never {
public static func sleep(for duration: Duration) async throws
}
During the pre-pitch work we flip-flopped a bit around if Duration should be per-clock or shared, making it per-clock was pedantically more strict but ended up causing a lot of pain when using it.
InstantProtocol should inherit Strideable, using Duration as a Stride.