【PHP Next】PHPにclamp関数がほしい

CCさくらとは何の関係もありません。

clampは何かというと、[CSSには既に実装されている](https://developer.mozilla.org/ja/docs/Web/CSS/clamp)のでそれを見るとわかりやすいですが、引数の数値を範囲内に納めてくれるというものです。
たとえば`clamp(min=100, max=200)`に150を渡すと150が返ってくる、0を渡すと100が返ってくる、300を渡すと200が返ってくる、という感じです。

ということでPHPにもclampがほしいというRFCが提出されました。
以下は該当のRFC、[PHP RFC: clamp](https://wiki.php.net/rfc/clamp)の紹介です。


# PHP RFC: clamp

## Introduction

clampは、値が範囲内にあるかをチェックします。
範囲内であればその値を返し、範囲外であれば境界値を返します。

ユーザランドの実装は幾つか存在しますが、[min](https://www.php.net/manual/en/function.min.php)や[max](https://www.php.net/manual/en/function.max.php)を使っているものも存在し、これらはコストがかかるため頻繁に呼ばれると遅くなります。
ユーザランド実装は呼び出しコストを考慮されていないものが多いので、言語での実装が望まれています。

## Proposal

このRFCでは、新しい関数clampを提案します。

```php
clamp ( int|float $num, int|float $min, int|float $max ) : int|float
```

3引数`$num`、`$min`、`$max`を取ります。
`$num`が`$min`と`$max`の間にあるかを調べ、範囲内であれば`$num`を返します。
範囲外であれば、`$min`と`$max`のうち近い方を返します。
すなわち、`$num > $max`であれば`$max`を返し、`$num < $min`であれば`$min`を返します。

もし`$max < $min`であれば、無効な値としてValueErrorを投げます。
何れかの値にNANが入ってきたときもValueErrorになります。

例としては以下のようになります。

```php
clamp(num: 1, min: 0, max: 3); // 1 範囲内
clamp(num: 1, min: 2, max: 5); // 2 小さい
clamp(num: 4, min: 1, max: 3); // 3 大きい

clamp(num: 0, min: 2, max: 1); // clamp(): Argument #2 ($min) cannot be greater than Argument #3 ($max)
```

## Backward Incompatible Changes

PHP自身の後方互換性のない変更はありません。
`clamp()`は関数名として使用できなくなります。

## Proposed PHP Version(s)

PHP8.x

## Open Issues

NANの取り扱いについて。
[#115085](https://externals.io/message/115076#115085) / [#115167](https://externals.io/message/115076#115167)


## Implementation

[プルリク](https://github.com/php/php-src/issues/7191)

## References

類似の実装。
[C++](https://en.cppreference.com/w/cpp/algorithm/clamp)
[CRAN](https://www.rdocumentation.org/packages/raster/versions/3.4-10/topics/clamp)
[CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/clamp)



# 感想

clampは[IEEE754-2019](https://ieeexplore.ieee.org/document/8766229)で規格が決められているそうです。
見れないからわかりませんが、こんなものも定義されているんですね。

この関数は、PHPでも容易に実装できます。

```php
function clamp(int|float $num, int|float $min, int|float $max):int|float {
    if($max < $min){
        throw new \ValueError('clamp(): Argument #2 ($min) cannot be greater than Argument #3 ($max)');
    }
    if($max < $num){
        return $max;
    }
    if($min > $num){
        return $min;
    }
    return $num;
}
```

[プルリクでのCによる実装](https://github.com/php/php-src/pull/7191/files#diff-4aabbf151a62ef59aab057549649dbf9ff6b4703740d7150fe37f4fcfaf30f71)とほぼ同じです。

```cpp
	if (zend_compare(zmin, zmax) > 0) {
		zend_argument_value_error(2, "must be smaller than or equal to argument #3 ($max)");
		RETURN_THROWS();
	}
	if (zend_compare(zmax, zvalue) == -1) {
		RETURN_COPY_VALUE(zmax);
	} else if (zend_compare(zvalue, zmin) == -1) {
		RETURN_COPY_VALUE(zmin);
	} else {
		RETURN_COPY_VALUE(zvalue);
	}
```

従って、どうしても必要な関数というわけではなく、[str_contains](https://www.php.net/manual/ja/function.str-contains.php)同様なくてもどうにかなるけどあれば便利なお助け関数という立ち位置ですね。

このRFC、2021年6月に提出され、その後一か月ほどでコードの実装は完成しました。
しかしその後ぱったりと消息が途絶え、進捗が停滞してしまいました。
[作者](https://github.com/thinkverse)は他のプロジェクトには様々にコミットしているので、飽きたか面倒臭くなったかでしょうか。

しかしその後も、これ欲しいよーというコメントがプルリクにちらほら追加されているので、もしかしたら今後レスキューされるかもしれません。
されないかもしれません。

ちなみに、以下がおそらくもっとも簡単な実装ですが、同時にRFCで言うところの遅い実装でしょう。

```php
function clamp(int|float $num, int|float $min, int|float $max):int|float {
    return max($min, min($max, $num));
}
```

ベンチ取ったわけじゃないからわかりませんが、見るからに遅そうですね。
しかし、この程度で有意差が出るほどclampを大量に呼び出すのっていったいどういうユースケースなんだろうか。