Hi Tim,
On 03.07.26 19:14, Tim Düsterhus wrote:
Hi
On 7/3/26 18:29, Pierre Joye wrote:
I like that new Duration, good new APIs. I was following the
discussion and one thing keeps coming to my mind as an API mistake:
public static function fromSeconds(int $seconds, int $nanoseconds
= 0)
Let me break down my thinking:
fromSeconds implies I'm setting a duration in seconds. But there are
two arguments — the second is nanoseconds, and milliseconds isn't
representable at all. The name promises one unit and the signature
quietly provides another.
Please see https://news-web.php.net/php.internals/131419; and the
fourth paragraph in the “design considerations”.
The $nanoseconds argument in the `fromSeconds()` constructor is
intentionally limited to “the number of nanoseconds in a second” which
means that the constructor can be reasoned about taking (fractional)
seconds - the base unit of Durations - as a fixed point decimal. I do
not consider this a break in the promise.
That's correct, but I don't think it applies here. The fromSeconds
issue is a naming defect regardless of language: the method name
promises "seconds" and the signature quietly also takes nanoseconds.
As mentioned above, the $nanoseconds parameter is intentionally
limited, making the constructor consistent with the other constructors
in that the “unit that is mentioned in the name” may overflow into the
larger units to allow construction with the desired precision in a way
that makes sense for the given use case.
Extending this pattern to e.g. `fromMinutes()` would result in
`fromMinutes($minutes, $seconds = 0)` with $seconds being limited to 59.
However in the other direction, `fromMilliseconds($milliseconds,
$microseconds = 0)`, with $microseconds <= 999 would feel weird,
because one wouldn’t say 5 milliseconds and 500 microseconds, when
they can just move the comma and say 5500 microseconds.
The seconds boundary is where the units shift from a “metric” factor
of 1000 to a factor of 60. Thus the special handling.
PHP already supports named arguments, giving us the same clarity as
Temporal object literal without borrowing any JS syntax:
Duration::from(seconds: -30, nanoseconds: -500);
Duration::from(seconds: -30, nanoseconds: 500); // error
Duration::from(seconds: 30, nanoseconds: 500).negate();
A single ::from() with named arguments would be unambiguous, easy to
remember, and remove the need for the whole family of from<Unit>
A design goal of the constructors was that they are injective
functions, i.e. there must be at most one way of creating a Duration
with a specific length from any given constructor. This is in order to
make it easy to reason about the resulting value, without needing to
perform (mental) calculations.
i.e. Duration::from(seconds: 1, milliseconds: 1500) would misleadingly
look “between 1 and 2 seconds” at a glance. But when each of the
“subunits” are restricted in magnitude, it would be very inconvenient
to construct values with the appropriate precision, because it would
require splitting inputs across the different parameters.
The `fromSeconds` constructor breaks the flow of all other constructors
as this is not only about seconds.
Also, this directly exposes how it's handled internally to the API which
makes it hard to extend later on (maybe we want to support picoseconds
on 10 years).
PHP should make things simpler - not more complex. There are plenty of
different duration systems out there a such values could come from as
input to a PHP application. The current Duration API forces you to
manually correctly calculate the value before passing it the constructor.
E.g.:
* time measurement using microtime(true)
* using hrtime(true) returns float on 32bit
* using JS (even Temporal) exposing integer values as "number" often
ends up as float in PHP
I totally understand that you want to keep it simple for now but the
$nanoseconds second argument makes it impossible to change in the future.
I like the idea of a general "from" constructor - but not restricted to
seconds + nanos but ALL supported units
public static function from(
int $hours = 0,
int $minutes = 0,
int $seconds = 0,
int $milliseconds = 0,
int $microseconds = 0,
int $nanoseconds = 0,
) : Duration
This makes it much more flexible, simpler to work with and extensible if
we want to increase the precision any time in the future.
It's intentional to start with the most uncommon duration unit of
$hours. That forces you to use named arguments which reads nicely and
handling floats or not can be discussed at a later time.
Duration::from(seconds: 100, microseconds: 500);
Duration::from(hours: 5, minutes: 30);
$seconds = 1;
$millis = 1_000;
$micros = 1_000_000;
$nanos = 1_000_000_000;
Duration::from(seconds: $seconds, milliseconds: $millis, microseconds:
$micros, nanoseconds: $nanos); // 4 seconds - yes please
> ... there must be at most one way of creating a Duration with a
specific length from any given constructor. This is in order to make it
easy to reason about the resulting value, without needing to perform
(mental) calculations.
It makes it simple to reason about for you as implementing it - it
forces the burden to the one using the API.
Regards,
Marc