Expose attosecond representation of Duration
01 何が問題だったのか
SE-0329 で導入された Duration は、秒とアト秒(10⁻¹⁸秒)からなる 128 ビット整数で時間の長さを表現する型です。内部的には _high: Int64 と _low: UInt64 に分割して 128 ビットを保持していますが、これを外から扱う API として公開されているのは次の 2 通りだけでした。
低位 / 高位ビットによる分解(underscored)
public struct Duration: Sendable {
public var _low: UInt64
public var _high: Int64
public init(_high: Int64, low: UInt64) { ... }
}
秒成分とアト秒成分への分解
extension Duration {
public var components: (seconds: Int64, attoseconds: Int64) { get }
public init(secondsComponent: Int64, attosecondsComponent: Int64)
}
前者はアンダースコア付きで、直接利用は想定されていません。後者は timespec などの既存 API との相互運用のためのもので、Duration が保持している値を「秒」と「その秒未満のアト秒」の 2 成分に分けて返す設計になっています。したがって、Duration が表す総アト秒数(=内部の 128 ビット整数そのもの)をそのまま受け渡す手段はありませんでした。
Duration 自体は SE-0329 の時点で 128 ビットのアト秒値として定義されていたものの、Swift には当時 128 ビット整数型がなく、「128 ビットとしての総アト秒」を公開型として表現できなかったという経緯があります。その後 Int128 が標準ライブラリに追加されたため、この制約が解消されました。
総アト秒を直接扱えないと、たとえば「指定した上限までのランダムな Duration を作る」といった処理が冗長になります。元 Proposal の例では次のようなコードが必要でした。
func randomDuration(upTo maxDuration: Duration) -> Duration {
let attosecondsPerSecond: Int128 = 1_000_000_000_000_000_000
let upperRange =
Int128(maxDuration.components.seconds) * attosecondsPerSecond
+ Int128(maxDuration.components.attoseconds)
let (seconds, attoseconds) =
Int128.random(in: 0..<upperRange).quotientAndRemainder(dividingBy: attosecondsPerSecond)
return .init(secondsComponent: Int64(seconds), attosecondsComponent: Int64(attoseconds))
}
components で秒とアト秒に分解し、Int128 に再構築して計算したあと、また秒とアト秒に分け直して Duration を作り直す、という変換が何度も必要で、冗長でミスが入り込みやすく、性能面でも不利でした。
02 どのように解決されるのか
Duration に、総アト秒を Int128 として直接読み書きする 3 つ目の API を追加します。既存の components / _high・_low はそのまま残り、追加のみで互換性を壊しません。
attoseconds computed property と init(attoseconds:)
次の 2 つが Duration に追加されます。
@available(SwiftStdlib 6.0, *)
extension Duration {
/// The duration represented in attoseconds.
public var attoseconds: Int128 {
Int128(_low: _low, _high: _high)
}
/// Initializes a `Duration` from the given number of attoseconds.
public init(attoseconds: Int128) {
self.init(_high: attoseconds._high, low: attoseconds._low)
}
}
attoseconds は Duration が内部で保持している 128 ビットのアト秒値そのものを Int128 として返します。init(attoseconds:) は Int128 値から直接 Duration を構築します。どちらもビット表現をそのまま橋渡しするだけで、スケーリングや丸めは発生しません。
これにより、先ほどのランダム生成の例は次のように大幅に短く書けます。
func randomDuration(upTo maxDuration: Duration) -> Duration {
return Duration(attoseconds: Int128.random(in: 0..<maxDuration.attoseconds))
}
総アト秒を Int128 としてそのまま扱えるので、秒/アト秒への分解・再構築の往復が不要になり、記述が簡潔になるだけでなく、Duration を使った高精度な算術(ランダム生成、比率計算、スケーリング、シリアライズなど)も書きやすくなります。
ファクトリメソッドではなくイニシャライザにした理由
Duration には nanoseconds(_:) や microseconds(_:) のような static func のファクトリメソッドが用意されており、attoseconds(_:) を同じ形で足すことも検討されました。しかし既存のファクトリは BinaryInteger と Double の両方のオーバーロードを持つのが通例で、アト秒の場合は次の理由からそれが噛み合いません。
- アト秒より細かい単位は表現できないため、
Doubleオーバーロードは意味を持ちません。 BinaryIntegerオーバーロードを用意すると、Int128以外からの変換でスケーリングや切り捨てが必要になり、「Int128をそのまま 128 ビット値として受け渡す」という今回の API の狙い(単純さと精度の保証)が崩れます。
結果として attoseconds(_:) は Int128 専用の浮いたメソッドになってしまい、他のファクトリとの対称性も崩れます。そこで、components 用の init(secondsComponent:attosecondsComponent:) と揃えて、イニシャライザの形で提供することが選ばれました。