µPitch: add `Duration.nanoseconds(_:)`

Duration has the following static methods:

public static func seconds<T: BinaryInteger>(_ seconds: T) -> Duration
public static func seconds(_ seconds: Double) -> Duration
public static func milliseconds<T: BinaryInteger>(_ milliseconds: T) -> Duration
public static func milliseconds(_ milliseconds: Double) -> Duration
public static func microseconds<T: BinaryInteger>(_ microseconds: T) -> Duration
public static func microseconds(_ microseconds: Double) -> Duration
public static func nanoseconds<T: BinaryInteger>(_ value: T) -> Duration

For no good reason, the obvious additional method:

public static func nanoseconds(_ nanoseconds: Double) -> Duration

was omitted from the proposal and implementation. It's somewhat useful. It's relatively simple to implement inside of Duration (where we can use _Int128), but relatively hard to implement well outside of the standard library. We should add it.

An implementation is available here.

35 Likes

I assume it was omitted because it’s always going to truncate? round? the value, and Duration is documented to not have sub-nanosecond precision. But it doesn’t seem harmful to have it when someone could just as easily say Duration(milliseconds: value / 1000) as Duration(nanoseconds: UInt64(value)).

2 Likes

To be clear, Duration has attosecond precision. The APIs that do stuff with durations generally do not.

6 Likes

I completely misremembered. Yeah, I can’t think of a reason either then.

Since we’re on the topic of omissions from these APIs, can we add the overloads that make + commutative (as Strideable already does, and thus all clock and duration types, but not instant types) as well as *?

extension InstantProtocol {
  // Exists:
  public static func + (_ lhs: Self, _ rhs: Duration) -> Self
  // Omitted:
  public static func + (_ lhs: Duration, _ rhs: Self) -> Self
}

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

extension Duration {
  // Exists:
  public static func * (_ lhs: Duration, _ rhs: Double) -> Duration
  public static func * (_ lhs: Duration, _ rhs: Int) -> Duration
  // Omitted:
  public static func * (_ lhs: Double, _ rhs: Duration) -> Duration
  public static func * (_ lhs: Int, _ rhs: Duration) -> Duration
}
2 Likes

Agree on the addition and that it would be good to do a holistic pass about what else is missing here and do it all together.

2 Likes

Assuming these don’t hurt typechecker performance, sure.

1 Like

Agreed, and I will, but we should be willing to take small proposals for obvious missing API without asking every would-be contributor to do this. The possibility of other improvements doesn’t have to hold up a small obvious change.

15 Likes

Missed opportunity to call this a nano-Pitch :sweat_smile:

15 Likes

Seems reasonable. Can this also be @backDeployed?

1 Like

Yes, it can be.

2 Likes

I thought about it, but a "nanopitch" feels more like one of our "change five words in a docc comment" proposals.

4 Likes

The problem with Strideable is that it would require SignedNumeric which requires being able to multiply two durations together and get the same type back (which is kinda nonsense)

Sorry, I’m not proposing Strideable conformance; I’m saying that those types in the proposal which already conform to Strideable have commutative +, but other types don’t have commutative +, and we can take the opportunity to address that inconsistency here because being able to add a duration to an instant in one order but not another is not an entirely intuitive limitation (or necessary, unless it messes up overload resolution performance).

4 Likes

What algorithms are used by typechecker or how does one know a particular change affects its performance?

We ask the CI system to measure compiler performance on a PR. This isn't perfect, but it's the best proxy we have.

I've read somewhere that there are reasons not to provide them, but from a practical point of view, I don't understand why minutes, hours, days, and weeks are missing. They have a pretty clear meaning and even if there exist calendars where their meanings might be different, with documentation their meanings can be made clear. I would optimize for the vast majority of use cases here.

I've actually defined a helper in my own library here that adds these APIs. So I thought maybe they should be considered (again) when doing a "holistic pass about what else is missing here".

days (and weeks) can have leap seconds. the duration API lets us advance instants by durations, and we probably don’t want people writing things like

let tomorrow:ContinuousClock.Instant = today.advanced(by: .days(1))

if you write

let tomorrow:ContinuousClock.Instant = today.advanced(
    by: .seconds(24 * 60 * 60))

you probably still have a bug, but at least it’s less surprising if today is 4:20 PM today and tomorrow isn’t 4:20 PM tomorrow.

2 Likes

I write this kind of code all the time. But I don't see it as a problem because I don't need second-level exactness. As a leap second is introduced less than once a year, it would require 60 years to make a difference of one minute. I see the point about exactness, but again, that could be solved with documentation, a warning or something else.

For a developer who doesn't know that there's something like a leap second, it's as surprising to them as if they used today.advanced(by: .days(1)). And if a developer knows about leap seconds, documenting that Duration.days isn't leap-second aware should be good enough, I think. I would guess that such a pure "Duration" representing API doesn't know anything about any calendrical things.

Just my noob opinion though, I'm no expert in this topic.