There is a lot to unpack here! I'm very excited to see this proposal come up.
Overall, I love the direction this pitch is pointing. Unifying the various temporal APIs to be more expressive and easier to understand and use is awesome and I'm thrilled this is being looked at.
Getting on to specific feedback:
What is a Clock?
I'm going to push back on the definition of a clock, which is described as:
an item to provide a concept of now plus a way to wake up after a given point in time
I'd argue that a clock is only the first part: the provider of "now". "Waking up" is a task that something does after consulting a clock. The function gets a clock, sees what "now" is, looks at when "wake up" time is, computes that duration, and then sleeps for that duration.
As a real-world example, if I want to sleep for 8 hours, I look at "now" on my clock and then set an alarm for 8 hours after now, and then I sleep; not the clock.
Thus, the entire definition of ClockProtocol
should be this:
public protocol ClockProtocol {
associatedtype Instant: InstantProtocol
func now() -> Instant
}
Sleeping is a task performed by the consumer of a clock; not the clock itself. The clock only tells you what time it is.
Clocks Provide the Concept of Now
This leads directly into the next point:
Why is static var now: Self
on InstantProtocol
? A clock tells you when "now" is; it is not something intrinsic to the type itself.
I see from later examples that having this enables short-hand syntax like .now.advanced(by: .seconds(3))
, but fundamentally this is the relying on the same sort of global-and-hidden dependency that Date()
does.
The opening definition of a clock is that "it provides a concept of now". That's good. If we want to know when "now" is, we should ask the clock. Yes it will make a couple of call-sites a little uglier or make something be two lines instead of one. But doing this would mean that we can re-use the same type of InstantProtocol
between different clocks, which is useful.
Different Kinds of Clocks
I love ClockProtocol
as a concept. In my own Time library, I have many different kinds of clocks, because manipulating time (especially while debugging) is really powerful. As a trivial example, if you want to test that something stays on-screen for 30 seconds and then disappears, you could do that by sitting around for 30 seconds and then seeing if it's gone, or you could use a clock that runs 30 times faster than normal and only wait around one second. Or if you're writing some code that wants to a cron-like job every 60 minutes, creating a clock that's offset from now so you can "jump ahead" to the proper time is another exceptionally handy thing.
Thus, the ability to create something like:
public struct ScaledClock<C: ClockProtocol>: ClockProtocol {
public typealias Instant = C.Instant
public let scaleFactor: Double
public let baseClock: C
...
}
public struct OffsetClock<C: ClockProtocol>: ClockProtocol {
public typealias Instant = C.Instant
public let offset: Duration
public let baseClock: C
...
}
... is immensely powerful and useful.
Where I run into an issue is with the associated type on Clock
. I understand the rationale about wanting to have a compile-time differentiation between Monotonic and Uptime and Wall instants, but having that associated type makes injecting a Clock extremely difficult. I can no longer do...
class MyCronJobScheduler {
var clock: ClockProtocol
}
... and then inject the proper dependency of a scaled, offset, or "normal" clock. Instead, I must make everything generic based on the Clock type, and that is painful.
Clocks like these also underscore why now
needs to be on the Clock, and not the Instant type. Asking a clock that's scaling WallClock
when now is would be a very very very different answer than asking the corresponding Instant
type directly. It's also something that's dependent on the scaling factor (2x, 30x, 0.5x, etc) and is therefore not something that even could be implemented on a ScaledInstant
type, unless you were willing to define a different type for every possible scaling factor, and down that path lies madness.
Converting Between Clocks
Having the ability to create custom clocks (which I've shown and experienced to be immensely useful) means that a clock technically has one other task: telling you how long a duration actually is.
Imagine a clock that's sped up by a factor of 2: for every 1 second that passes on the wall clock, two seconds pass on the scaled clock.
Let's also then say I want to perform something 10 seconds from now based on that clock. So, I get now()
from the clock and advance the instant by 10 seconds.
I cannot Task.sleep
for 10 real seconds, because that will be 20 seconds on the scaled clock. So, I need the clock itself to convert this into a "real time" Duration that includes any scaling factors.
This brings the (proposed) definition of ClockProtocol
to this:
public protocol ClockProtocol {
associatedtype Instant: InstantProtocol
func now() -> Instant
// default implementation returns the parameter
// this would only need to be implemented by a clock that runs slower or faster than real time
func absoluteDuration(for clockDuration: Duration) -> Duration
}
This is also a good example for why ClockProtocol
can't have static methods for sleeping or computing durations. Different ClockProtocol
concretions can't always have the requisite information available statically.
Human Temporal Units
I saw that Duration
has these convenience bits:
extension Duration {
public static func hours(_ hours: Int) -> Duration
public static func hours(_ hours: Double) -> Duration
public static func minutes(_ minutes: Int) -> Duration
public static func minutes(_ minutes: Double) -> Duration
}
I would recommend leaving these off. Seconds are one of the fundamental base units of measurement. Milliseconds, microseconds, and nanoseconds are easy extrapolations off that.
Minutes and hours are human constructs and are not part of the SI system. These sort of definitions should be at the Foundation level, where Calendar
and Locale
exist to give these names their proper meaning.
This is getting really long, so I'm going to cut it off here. Some of this stuff has been mentioned by others already, so I apologize for repeating some points.
Again, I'm really pleased that this is getting pitched and I'm excited to see this evolve.