[RFC] [Discussion] Add clamp function

php.internals

Kyle Katarn

1 year ago
Hello, I handled the feedback received on the draft RCF https://wiki.php.net/rfc/clamp_v2 If I didn't forget anything this should be now ready for discussion. So I updated its status. There is an implementation proposal: https://github.com/php/php-src/pull/19434 And some draft for documentation here: https://github.com/php/doc-en/pull/4814 Thanks,

Marco Pivetta

1 year ago
Hey Kyle, On Wed, 27 Aug 2025 at 15:35, Kyle Katarn <kylekatarnls@gmail.com> wrote:
> Hello, > > I handled the feedback received on the draft RCF > https://wiki.php.net/rfc/clamp_v2 >
I know this isn't very used in internals, but does it make sense to write generics for the generated stubs of this function? Specifically, I'd expect input, min and max to be bound to the same exact type: ```php /** * @template T * @param T $value * @param T $min * @param T $max * @return T */ function clamp (mixed $value, mixed $min, mixed $max): mixed { // ... } ``` Greets, Marco Pivetta https://mastodon.social/@ocramius https://ocramius.github.io/

hakre

1 year ago
On Wednesday 27 August 2025 17:45:59 (+02:00), Marco Pivetta wrote:
> I know this isn't very used in internals, but does it make sense to
write
> generics for the generated stubs of this function? > [...] > /** > * @template T > * @param T $value > * @param T $min > * @param T $max > * @return T > */ > function clamp (mixed $value, mixed $min, mixed $max): mixed {
+1 This is an excellent suggestion. Using a template ensures type safety and clearly communicates the intended relationship between the arguments and return value, which is a significant improvement. This also highlights a minor but important clarification for the documentation. To make the behavior perfectly unambiguous, I suggest a small edit to explicitly state the bounds are inclusive. Current text:
> `clamp` takes three arguments, a `$value`, `$min` and `$max`, then > checks if `$value` is within the bounds of `$min` and `$max`.
Suggested edit:
> `clamp` takes three arguments, a `$value`, `$min` and `$max`, then > checks if `$value` is within the bounds of `$min` and `$max` (both > inclusive).
This is a minimal change that adds crucial clarity already in the first sentence. --hakre

Tim Düsterhus

1 year ago
Hi Am 2025-08-28 08:35, schrieb Hans Krentel:
> This also highlights a minor but important clarification for the > documentation. To make the behavior perfectly unambiguous, I suggest > a small edit to explicitly state the bounds are inclusive.
This is a good point and I agree that it should be spelled out explicitly. However to me this also raises the question if the interval boundaries should be configurable to switch between closed intervals and (half-)open intervals. For many applications of continuous values a right-open interval (excluding the upper boundary) is more useful, since this makes it easy to generate non-overlapping ranges (e.g. to clamp to a given day when passing DateTimeImmutable). This would also provide an additional value-add over the min-max construction, where the boundaries would need to be calculated manually, which is super complicated to do in userland for floats. There's precedent in ext/random with the Randomizer::getFloat() method that takes a Random\IntervalBoundary enum to decide on the the interpretation of the boundary values. Translated to `clamp()` this could mean: $value = 1.0; clamp($value, 0.0, 1.0, IntervalBoundary::ClosedOpen); // 0.999999999999999888978 Best regards Tim Düsterhus

Weedpacket

1 year ago
On 2025-08-28 19:37, Tim Düsterhus wrote:
> > There's precedent in ext/random with the Randomizer::getFloat() method > that takes a Random\IntervalBoundary enum to decide on the the > interpretation of the boundary values. Translated to `clamp()` this > could mean: >
If it weren't living in the Random extension, that very Enum could have been used.

Tim Düsterhus

1 year ago
Hi
>> There's precedent in ext/random with the Randomizer::getFloat() method >> that takes a Random\IntervalBoundary enum to decide on the the >> interpretation of the boundary values. Translated to `clamp()` this >> could mean: >> > If it weren't living in the Random extension, that very Enum could have > been used.
Indeed. Back when introducing the enum, we actually had a discussion about naming and “placement”. In the end, we opted to make it ext/random-specific as a more conservative choice. I think back then it was the first native enum. See https://externals.io/message/118810#118910 and https://chat.stackoverflow.com/transcript/11?m=55444127#55444127. If a new general-purpose IntervalBoundary enum is introduced, I'm happy to widen the type in ext/random from `\Random\IntervalBoundary` to `\IntervalBoundary|\Random\IntervalBoundary` (and deprecate the latter in one of the following versions). Best regards Tim Düsterhus

hakre

1 year ago
On Thursday 28 August 2025 13:28:43 (+02:00), Tim Düsterhus wrote:
> Hi > > >> There's precedent in ext/random with the Randomizer::getFloat() method that takes a Random\IntervalBoundary enum to decide on the the interpretation of the boundary values. Translated to `clamp()` this could mean: > >> > If it weren't living in the Random extension, that very Enum could have been used. > > Indeed. Back when introducing the enum, we actually had a discussion about naming and “placement”. In the end, we opted to make it ext/random-specific as a more conservative choice. I think back then it was the first native enum. > > See https://externals.io/message/118810#118910 and https://chat.stackoverflow.com/transcript/11?m=55444127#55444127. > > If a new general-purpose IntervalBoundary enum is introduced, I'm happy to widen the type in ext/random from `\Random\IntervalBoundary` to `\IntervalBoundary|\Random\IntervalBoundary` (and deprecate the latter in one of the following versions). > > Best regards > Tim Düsterhus >
Not sure if I'm too much off track here: As we can't alias classes in userland that are internal classes, internally PHP could do the oppsite and given clamp could borrow it out to/from \Random\IntervalBoundary given we have enough discipline. That also could buy some time on the widening idea and preserve a bit of the conservative approach to further on defer the details until necessary. Widening is certainly correct in its own right, but why not keep the door open this way so one day we can applaud the first internal enum for its foresight? -- hakre

Niels Dossche

1 year ago
> Not sure if I'm too much off track here: As we can't alias classes in userland that are internal classes, internally PHP could do the oppsite and given clamp could borrow it out to/from \Random\IntervalBoundary given we have enough discipline. That also could buy some time on the widening idea and preserve a bit of the conservative approach to further on defer the details until necessary.
This is false: you can perfectly make a userland alias of an internal class since PHP 8.3. PHP can also make aliases of classes internally.

Kyle Katarn

1 year ago
Hi, 2025-08-28 at 09:37, Tim Düsterhus <tim@bastelstu.be> wrote :
> Hi > > Am 2025-08-28 08:35, schrieb Hans Krentel: > > This also highlights a minor but important clarification for the > > documentation. To make the behavior perfectly unambiguous, I suggest > > a small edit to explicitly state the bounds are inclusive. > > This is a good point and I agree that it should be spelled out > explicitly. However to me this also raises the question if the interval > boundaries should be configurable to switch between closed intervals and > (half-)open intervals. For many applications of continuous values a > right-open interval (excluding the upper boundary) is more useful, since > this makes it easy to generate non-overlapping ranges (e.g. to clamp to > a given day when passing DateTimeImmutable). This would also provide an > additional value-add over the min-max construction, where the boundaries > would need to be calculated manually, which is super complicated to do > in userland for floats. > > There's precedent in ext/random with the Randomizer::getFloat() method > that takes a Random\IntervalBoundary enum to decide on the the > interpretation of the boundary values. Translated to `clamp()` this > could mean: > > $value = 1.0; > clamp($value, 0.0, 1.0, IntervalBoundary::ClosedOpen); // > 0.999999999999999888978 > > Best regards > Tim Düsterhus >
Just my opinion but the function aims to provide an alternative to min(max()) that would be: - easier to read - faster to execute If having many different behaviors depending on an option and so a lot of if-else path complexity, then it will tend to defeat the second goal. I would add it if there is a strong interest, otherwise I'd rather stick to a minimalist implementation (like seen in most languages where clamp() is available.

Larry Garfield

1 year ago
On Thu, Aug 28, 2025, at 1:26 PM, Kyle Katarn wrote:
> Hi, > > 2025-08-28 at 09:37, Tim Düsterhus <tim@bastelstu.be> wrote :
>> There's precedent in ext/random with the Randomizer::getFloat() method >> that takes a Random\IntervalBoundary enum to decide on the the >> interpretation of the boundary values. Translated to `clamp()` this >> could mean: >> >> $value = 1.0; >> clamp($value, 0.0, 1.0, IntervalBoundary::ClosedOpen); // >> 0.999999999999999888978 >> >> Best regards >> Tim Düsterhus > > Just my opinion but the function aims to provide an alternative to > min(max()) that would be: > - easier to read > - faster to execute > > If having many different behaviors depending on an option and so a lot > of if-else path complexity, then it will tend to defeat the second > goal. I would add it if there is a strong interest, otherwise I'd > rather stick to a minimalist implementation (like seen in most > languages where clamp() is available.
I would also favor the minimalist/traditional/like-everyone-else approach. I wondered at first if open/closed boundaries made sense, but on consideration, that's not what clamp() traditionally does. It's just min/max shortcutting. So let's just do min/max shortcutting and move on with life. --Larry Garfield

Tim Düsterhus

1 year ago
Hi On 8/28/25 20:26, Kyle Katarn wrote:
> Just my opinion but the function aims to provide an alternative to > min(max()) that would be: > - easier to read > - faster to execute > > If having many different behaviors depending on an option and so a lot of > if-else path complexity, then it will tend to defeat the second goal. I
Adding an additional parameter to switch between 4 modes of comparison hardly qualifies as “a lot of if-else path complexity”. That can be done with a single `switch()` statement. I doubt that you can measure a meaningful difference in speed and if you can, then it's still possible to optimize based on the expected path (i.e. closed intervals). Since a big part of your argument in favor of the clamp() function is “performance” (both in your reply here and in the introduction to the RFC): Please add meaningful benchmarks to the RFC text that show how much faster clamp() is actually going to be compared to alternative solutions.
> would add it if there is a strong interest, otherwise I'd rather stick to a > minimalist implementation (like seen in most languages where clamp() is > available.
Best regards Tim Düsterhus

Weedpacket

1 year ago
On 2025-08-29 08:36, Tim Düsterhus wrote:
> > > Since a big part of your argument in favor of the clamp() function is > “performance” (both in your reply here and in the introduction to the > RFC): Please add meaningful benchmarks to the RFC text that show how > much faster clamp() is actually going to be compared to alternative > solutions. >
Another part of the argument is "complexity", as brought up in the original proposal. I notice that the offered implementation of this function relies entirely on zend_compare(). max/min special-case long and double arguments and compare them more directly, leaving cases like string/string, string/double, array/array, or boolean/null to be handled by zend_compare(). It is a lot more complex, turning less than a dozen lines (looping over and passing args to zend_compare()) into more than fifty, presumably for performance reasons. The extra complication may not be worth it if there are only two or three values being compared (long and double are the first types zend_compare tries).

Tim Düsterhus

1 year ago
Hi On 8/29/25 04:42, Morgan wrote:
> Another part of the argument is "complexity", as brought up in the > original proposal. I notice that the offered implementation of this > function relies entirely on zend_compare().
I'm not finding the word “complexity” in https://wiki.php.net/rfc/clamp or https://externals.io/message/115050. Can you link to the message you are referring to?
> reasons. The extra complication may not be worth it if there are only > two or three values being compared (long and double are the first types > zend_compare tries).
I'm positive that the benchmarks will answer that question. Best regards Tim Düsterhus

Weedpacket

1 year ago
On 2025-08-30 01:10, Tim Düsterhus wrote:
> Hi > > On 8/29/25 04:42, Morgan wrote: >> Another part of the argument is "complexity", as brought up in the >> original proposal. I notice that the offered implementation of this >> function relies entirely on zend_compare(). > > I'm not finding the word “complexity” in https://wiki.php.net/rfc/clamp > or https://externals.io/message/115050. Can you link to the message you > are referring to? >
I'm probably misremembering https://externals.io/message/128505#128532 and #128542.
>> reasons. The extra complication may not be worth it if there are only >> two or three values being compared (long and double are the first types >> zend_compare tries). > > I'm positive that the benchmarks will answer that question. >
Agreed. But if clamp doesn't need to special-case numeric inputs to perform well then maybe max/min don't either (at least not any more) and their implementations could be streamlined.

Kyle Katarn

358 days ago
Hello 2025-08-29 at 15:12, Tim Düsterhus <tim@bastelstu.be> wrote:
> Hi > > On 8/29/25 04:42, Morgan wrote: > > Another part of the argument is "complexity", as brought up in the > > original proposal. I notice that the offered implementation of this > > function relies entirely on zend_compare(). > > I'm not finding the word “complexity” in https://wiki.php.net/rfc/clamp > or https://externals.io/message/115050. Can you link to the message you > are referring to? > > > reasons. The extra complication may not be worth it if there are only > > two or three values being compared (long and double are the first types > > zend_compare tries). > > I'm positive that the benchmarks will answer that question. > > Best regards > Tim Düsterhus >
I back-ported "not cost-effective when called multiple times" from the clamp v1, I think its author meant "compared to user-land implementation using min(max())" So I just did this benchmark: https://github.com/kylekatarnls/clamp-perf-test/actions/runs/17546601109/job/49829393835 (following Tim's advice to use hyperfine for it). Summary is: php clamp.php ran 1.06 ± 0.02 times faster than php ternary.php 1.42 ± 0.03 times faster than php min-max.php Which means if everything is correct, then the current native implementation proposal is barely faster than a user-land function using ternary implementation. Using a user-land function and min(max()) is 42% slower. (See the files at the root of this repo: https://github.com/kylekatarnls/clamp-perf-test) Those numbers might have been better in the first implementation (which supported only int and float and it had no special case handling for NAN, so I guess it was even a bit faster this way). Anyway, I think the speed is probably not the main reason why we should add it. Maybe I would rather phrase the above result this way: - User-land implementation for clamp using ternary is notably faster than min(max()) user-land implementation -> but still the difference shouldn't be a concern for most people. - With the native implementation, you get (for no extra speed cost): -> NAN handling -> verification than min < max -> a standard way (framework/application can drop their own implementations and converge to the one propose by the language which will also be pretty similar with most implementations in other languages) So I guess I should arrange the note about cost-effectiveness, or maybe drop it all together.

Tim Düsterhus

1 year ago
Hi Am 2025-08-27 17:45, schrieb Marco Pivetta:
> I know this isn't very used in internals, but does it make sense to > write > generics for the generated stubs of this function?
The stub generator makes use of the types provided in the PHPDoc comment to generate optimizer information. The stub generator has no support for generic types (or things like array shapes), which makes this risky. The information is not used, it is not tested and it might confuse the stub generator. See https://github.com/php/php-src/pull/19546 as an example. If there is an interest in having generics in the stubs, the stub generator should first be extended to handle them, even if it does not (yet) use the information and then it should be consistently applied to the entirety of PHP's stdlib. Best regards Tim Düsterhus

Bob Weinand

1 year ago
Hey, On 27.8.2025 15:34:53, Kyle Katarn wrote:
> Hello, > > I handled the feedback received on the draft RCF > https://wiki.php.net/rfc/clamp_v2 > > If I didn't forget anything this should be now ready for discussion. > So I updated its status. > > There is an implementation proposal: > https://github.com/php/php-src/pull/19434 > And some draft for documentation here: > https://github.com/php/doc-en/pull/4814 > > Thanks,
As others have noted before, what's the motivation of having the to-be-clamped value as first parameter rather than infixed between min and max? E.g. from CSS: https://developer.mozilla.org/en-US/docs/Web/CSS/clamp: |clamp(min, val, max)| Feels to me as the most natural order, too. You mention it was taken from the first RFC, but that one also did not discuss the ordering in the first place either. Bob

Larry Garfield

1 year ago
On Wed, Aug 27, 2025, at 11:06 AM, Bob Weinand wrote:
> Hey, > > On 27.8.2025 15:34:53, Kyle Katarn wrote: >> Hello, >> >> I handled the feedback received on the draft RCF https://wiki.php.net/rfc/clamp_v2 >> >> If I didn't forget anything this should be now ready for discussion. So I updated its status. >> >> There is an implementation proposal: https://github.com/php/php-src/pull/19434 >> And some draft for documentation here: https://github.com/php/doc-en/pull/4814 >> >> Thanks, > > > As others have noted before, what's the motivation of having the > to-be-clamped value as first parameter rather than infixed between min > and max? > > E.g. from CSS: https://developer.mozilla.org/en-US/docs/Web/CSS/clamp: > > `clamp(min, val, max)` > > Feels to me as the most natural order, too. > > > > You mention it was taken from the first RFC, but that one also did not > discuss the ordering in the first place either.
Some quick data points from momentary googling: CSS: clamp(min, val, max) Python: clamp(val, min, max) C++: clamp(val, min, max) Java: clamp(val, in, max) Javascript: clamp(val, min, max) C#: clamp(val, min, max) Kotlin: val.clamp(min, max) (as an extension function) Ruby: val.clamp(min, max) So it looks like CSS is the oddball here. We should follow the clear majority approach. (Kyle, feel free to include this in the RFC.) --Larry Garfield

Bob Weinand

1 year ago
Thanks Larry! On 27.8.2025 18:40:51, Larry Garfield wrote:
> On Wed, Aug 27, 2025, at 11:06 AM, Bob Weinand wrote: >> Hey, >> >> On 27.8.2025 15:34:53, Kyle Katarn wrote: >>> Hello, >>> >>> I handled the feedback received on the draft RCF https://wiki.php.net/rfc/clamp_v2 >>> >>> If I didn't forget anything this should be now ready for discussion. So I updated its status. >>> >>> There is an implementation proposal: https://github.com/php/php-src/pull/19434 >>> And some draft for documentation here: https://github.com/php/doc-en/pull/4814 >>> >>> Thanks, >> >> As others have noted before, what's the motivation of having the >> to-be-clamped value as first parameter rather than infixed between min >> and max? >> >> E.g. from CSS: https://developer.mozilla.org/en-US/docs/Web/CSS/clamp: >> >> `clamp(min, val, max)` >> >> Feels to me as the most natural order, too. >> >> >> >> You mention it was taken from the first RFC, but that one also did not >> discuss the ordering in the first place either. > Some quick data points from momentary googling: > > CSS: clamp(min, val, max) > > Python: clamp(val, min, max) > C++: clamp(val, min, max) > Java: clamp(val, in, max) > Javascript: clamp(val, min, max) > C#: clamp(val, min, max) > > Kotlin: val.clamp(min, max) (as an extension function) > Ruby: val.clamp(min, max) > > So it looks like CSS is the oddball here. We should follow the clear majority approach. (Kyle, feel free to include this in the RFC.) > > --Larry Garfield
Indeed, that makes sense then. I must admit that I've only ever been using the clamp from CSS, but never from other languages. (I still think value in the middle makes more sense, but I guess that's me then, and I won't object.) Then yes, please note it in the RFC :-) Bob

Yitzchok Willroth

1 year ago
On Wed, Aug 27, 2025, 12:44 PM Larry Garfield <larry@garfieldtech.com> wrote:
> On Wed, Aug 27, 2025, at 11:06 AM, Bob Weinand wrote: > > Hey, > > > > On 27.8.2025 15:34:53, Kyle Katarn wrote: > >> Hello, > >> > >> I handled the feedback received on the draft RCF > https://wiki.php.net/rfc/clamp_v2 > >> > >> If I didn't forget anything this should be now ready for discussion. So > I updated its status. > >> > >> There is an implementation proposal: > https://github.com/php/php-src/pull/19434 > >> And some draft for documentation here: > https://github.com/php/doc-en/pull/4814 > >> > >> Thanks, > > > > > > As others have noted before, what's the motivation of having the > > to-be-clamped value as first parameter rather than infixed between min > > and max? > > > > E.g. from CSS: https://developer.mozilla.org/en-US/docs/Web/CSS/clamp: > > > > `clamp(min, val, max)` > > > > Feels to me as the most natural order, too. > > > > > > > > You mention it was taken from the first RFC, but that one also did not > > discuss the ordering in the first place either. > > Some quick data points from momentary googling: > > CSS: clamp(min, val, max) > > Python: clamp(val, min, max) > C++: clamp(val, min, max) > Java: clamp(val, in, max) > Javascript: clamp(val, min, max) > C#: clamp(val, min, max) > > Kotlin: val.clamp(min, max) (as an extension function) > Ruby: val.clamp(min, max) > > So it looks like CSS is the oddball here. We should follow the clear > majority approach. (Kyle, feel free to include this in the RFC.) > > --Larry Garfield >
A reference from userland, Laravel has Number::clamp($val, $min, $max) as well. https://github.com/laravel/framework/blob/12.x/src%2FIlluminate%2FSupport%2FNumber.php#L301

hakre

1 year ago
On Wednesday 27 August 2025 18:40:51 (+02:00), Larry Garfield wrote:
> ...So it looks like CSS is the oddball here. We should follow the clear > majority approach...
While the majority of languages favor `clamp($value, $min, $max)`, dismissing the CSS order without examining the "why" misses a valuable perspective, especially for a language so intertwined with web development. The rationale for aligning with the majority (and the current RFC) boils down to the **Principle of Least Surprise within PHP's own ecosystem**. PHP is an imperative language. Its standard library functions, particularly in math, consistently take the value to be operated on as the first argument: `min($value1, $value2)`, `max($value1, $value2)`, `round($value, $precision)`. Introducing `clamp($min, $value, $max)` would break this pattern, making it the odd one out *within PHP itself*. This internal inconsistency would be a greater source of errors for PHP developers than not matching an external standard like CSS. The solution to the CSS dissonance is not to change the function but to **document it explicitly**:
> **Note for Web Developers:** This function uses the argument order > `clamp(value, min, max)`, which differs from the CSS function > `clamp(min, value, max)`.
This addresses the concern head-on. The ultimate win is not the argument order itself, but the mere existence of the function, which finally banishes the awkward and error-prone `max($min, min($max, $value))` incantation. My 2cents, --hakre

Kyle Katarn

345 days ago
Hi, 2025-08-27 at 15:34, Kyle Katarn <kylekatarnls@gmail.com> wrote:
> Hello, > > I handled the feedback received on the draft RCF > https://wiki.php.net/rfc/clamp_v2 > > If I didn't forget anything this should be now ready for discussion. So I > updated its status. > > There is an implementation proposal: > https://github.com/php/php-src/pull/19434 > And some draft for documentation here: > https://github.com/php/doc-en/pull/4814 > > Thanks, >
If there is nothing left to discuss, I'd like to open the votes for this function adding.