ストップウォッチを開始してから止めるまでみたいな、単純な経過時間という概念がほしい。
sleep(1);
usleep(1000000);
time_nanosleep(0, 1000000000);
$dt2 = $dt->add(new DateInterval('PT1S'));
関数によって引数の単位がバラバラです。
もっとこう、時間経過クラスみたいなのを一括で扱えると便利では?
いまのPHPに存在するものとしてはDateIntervalが最もそれに近いですが、ただDateIntervalには月の概念が存在するため、たとえば一か月は何日なんだよという問いに単独で答えることができません。
基本的に他のDateTimeクラスとセット運用される前提となっています。
そんなわけで、単純に『経過秒数』という概念を表すクラス\Time\Durationが提案されました。
残念ながらこのRFCには含まれていないみたいですが、今後はこのような書き方ができるようになる予定です。
sleep(\Time\Duration::fromSeconds(1));
usleep(\Time\Duration::fromSeconds(1));
time_nanosleep(\Time\Duration::fromSeconds(1));
$dt2 = $dt->add(\Time\Duration::fromSeconds(1));
もちろん既存の書き方をそのまま使うこともできます。
以下は対象のRFC、Duration classの紹介です。
Duration class
Introduction
経過時間の定義と計測は、プログラミング言語でよくある概念です。
経過時間を必要とするユースケースとしては、ネットワークリクエストのタイムアウト、再送待機時間、定期実行する操作などが挙げられます。
PHPでは、ストップウォッチの時間のように構造化された時間を表現する方法がまだありません。
既存のAPIでは、秒・ミリ秒・ナノ秒など個々のユースケースに適しているそれぞれの単位が使用されています。
また浮動小数が使われる場合もありますが、これは精度の理由で問題が生じる可能性があります。
DateIntervalは期間の定義には適していません。
なぜならば、夏時間や月の長さの違いなどにより、これを正確に秒数にマッピングすることができないからです。
このRFCではストップウォッチやエッグタイマー等で計測した機関をナノ秒単位で構造的に表現するクラス、Time\Durationクラスを導入することを提案します。
// 0.5秒sleepする
sleep(\Time\Duration::fromMilliseconds(500));
Proposal
Time\Durationクラスを追加します。
静的コンストラクタの引数は全て0以上。
final readonly class Duration
{
/* 秒 最大9_223_372_035 */
public readonly int $seconds;
/* ナノ秒 */
public readonly int $nanoseconds;
/* 符号 0の場合false */
public readonly bool $negative;
/**
* 秒・ナノ秒から生成
*/
public static function fromSeconds(int $seconds, int $nanoseconds = 0): self{}
/**
* ナノ秒から生成
*/
public static function fromNanoseconds(int $nanoseconds): self{}
/**
* マイクロ秒から生成
*/
public static function fromMicroseconds(int $microseconds): self{}
/**
* ミリ秒から生成
*/
public static function fromMilliseconds(int $milliseconds): self{}
/**
* 分から生成
*/
public static function fromMinutes(int $minutes): self{}
/**
* 時間から生成
*/
public static function fromHours(int $hours): self{}
/**
* ISO-8601文字列から生成、時間以下の単位を利用
*/
public static function fromIso8601DurationString(string $specification): self{}
/**
* 符号を反転する
*/
public function negate(): self{}
/**
* 絶対値を返す
*/
public function absolute(): self{}
/**
* 加算
*/
public function add(self $duration): self{}
/**
* 減算
*/
public function sub(self $duration): self{}
/**
* 乗算
* $factorは0以上
*/
public function multiplyBy(int $factor): self{}
/**
* 割り算。ナノ秒の端数は切り捨て。
* $divisorは0以上
*/
public function divideBy(int $divisor): self{}
/**
* 比較。 return $a > $bなら1、=なら0、<なら-1
*/
public static function compare(self $a, self $b): int{}
}
比較ハンドラは実装するので、>などの比較は使用可能です。
その他の+演算子などは使用できません。
またThrowableポリシーに従い、Time\TimeException例外が追加されます。
The new polling API
このRFCがPHP8.6で受理された場合、PHP8.6で受理されたポーリングAPIは、引数に本クラスを使用するよう調整されます。
namespace Io\Poll;
final class Context
{
public function wait(
?Time\Duration $timeout = null,
?int $maxEvents = null
): array {}
}
Passing Duration objects to APIs unable to handle nanosecond precision
ナノ精度を扱えない対象に対してナノ精度のTime\Durationを与えた場合の動作については、対象のAPIに委ねられます。
ユースケースによって動作が異なる可能性があるからです。
考えられる選択肢の例としては、以下のようになりです。
・切り上げ。sleep()などはこれが適しています。
・切り捨て。
・例外をスローする。誤差が許容できない場合。
Design Considerations
設計について。
Time\Durationは、PHPの次世代日時ライブラリの嚆矢であり、構成要素の基礎部分となることを意図していますが、これ単体でも価値を提供します。
その設計は、Goのtime.Duration、Javaのjava.time.Duration、Rustのstd::time::Durationを大まかに踏襲しています。
単位が$secondsと$nanosecondsに分かれているのは、人間同士の基本単位として主に秒が使われていることに合わせています。
また符号付き32ビット整数は1ナノ秒をあらわすのに十分な大きさです。
秒を分けることで、32ビットOSでも約68年の間隔を表すことができます。
将来的には単一の64ビット整数でナノ秒を表現する想定であるため、$secondsの最大値は64ビットOSについても9_223_372_035、約262年に制限されます。
負のDurationはプロパティ$negativeで示されます。
これにより、$seconds・$nanosecondsのどちらがマイナスになるべきかといった曖昧さを回避されます。
意図的に負の期間を生成するには、->negate()を呼び出す必要があります。
そのような利用法は稀であると考えられるからです。
今後日時ライブラリが追加された場合は、日時の差分を計算する場合に負のDurationが自然に発生すると考えられます。
Examples
このRFCでは、既存の関数がDurationを受け入れるようにする提案は含まれていません。
use Time\Duration;
$oneSecond = Duration::fromSeconds(1);
$halfSecond = $oneSecond->divideBy(2);
$onePointFiveSeconds = $oneSecond->add($halfSecond);
// 1.5秒スリープ
sleep($onePointFiveSeconds);
$negativeHour = Duration::fromHours(1)->negate();
$durations = [
$oneSecond,
$negativeHour,
$onePointFiveSeconds,
$halfSecond,
];
// [-1h, 0.5s, 1s, 1.5s]になる
usort($durations, Duration::compare(...));
$baseDelay = Duration::fromMilliseconds(100);
$attempt = 5;
$delay = $baseDelay->multiplyBy(2 ** $attempt); // 3.2秒になる
Backward Incompatible Changes
互換性のない変更。
このRFCでは、標準ライブラリのtimeと同名の新しいクラスを導入します。
GitHubにはTime\Durationの使用例が7件存在します。
https://github.com/marc-mabe/php-timelib/blob/5092160dfae9c3bf9484a6c86c6809688f7341bd/polyfill/Duration.php#L21
https://github.com/jiripudil/php-ext-time-prototype/blob/06b97b9e0c8f4878d68aee3a7571fc079f3ebc17/polyfill/src/Duration.php#L16
Proposed PHP Version(s)
PHP8.6.
Open Issues
未解決の問題はありません。
Rejected Features
却下された機能。
・複数のコンストラクタではなく、列挙型引数を持つコンストラクタをひとつだけ実装する。
・hoursAndMinutesのように、複数の引数を渡すコンストラクタを実装する。
基本単位であるfromSecondsは例外です。
感想
これ便利なのでほしい。
Externalsでも概ね高評価です。
ただ議論になっている仕様が幾つかありまして、そのひとつが負数の内部的扱いです。
RFCの仕様では『秒』『ナノ秒』『符号』という形でデータを持っているのですが、これは設計上正しくないから『符号+秒』『ナノ秒』にすべきではないか、という主張がありました。
ただ、たとえば-500ナノ秒を表す場合、前者なら『0秒』『500ナノ秒』『マイナス』となるわけですが、後者だと『マイナス1秒』『999999500ナノ秒』になってしまってすごいわかりにくいので正直辞めてほしいですね。
そんなかんじでいくつか懸念はあるみたいですが、おそらく投票まで行けば受理されるとは思います。
PHP8.6は期限的に厳しいかもしれませんが、8.7あたりには実装されてほしいところですね。