【PHP8.3】array_sumにGMPを突っ込めるようになる

[GMP](https://www.php.net/manual/ja/ref.gmp.php)インスタンスは演算子オーバーロードされているため、普通の数値と混ぜて計算が可能です。

```php
echo gmp_init("123") + 456; // 579

echo array_sum([gmp_init("123"), 456]); // 456 ← ？？？
```

なぜか[array_sum](https://www.php.net/manual/ja/function.array-sum.php)や[array_product](https://www.php.net/manual/ja/function.array-product.php)ではGMPが正しく計算されてくれません。
実はこれらの関数、算術演算子に処理を渡しているわけではなく独自の演算を行っています。
そのせいで異なる計算結果となり、これではわかりにくくて困ってしまいますね。

ということで、`array_sum|product`の計算方法を算術演算子と同じにしようというRFCが提出されました。
以下は該当のRFC、[Saner array_(sum|product)()](https://wiki.php.net/rfc/saner-array-sum-product)の紹介です。


# [Saner array_(sum|product)()](https://wiki.php.net/rfc/saner-array-sum-product)

## Introduction

PHPの標準関数[array_sum](https://www.php.net/manual/ja/function.array-sum.php)および[array_product](https://www.php.net/manual/ja/function.array-product.php)は、入力の配列を演算して折り畳む高階関数です。
PHPはまた、任意の演算で配列の折り畳みを行う[array_reduce](https://www.php.net/array_reduce)をサポートしています。
そのため、先の2関数をユーザランドで実装することも可能です。

```php
// $output = array_sum($input)
$output = array_reduce($input, fn($carry, $value) => $carry + $value, 0);

// $output = array_product($input)
$output = array_reduce($input, fn($carry, $value) => $carry * $value, 1);
```

入力値の型が`int`と`float`だけであれば、組込関数とユーザランド実装は完全に同じ動作になります。
しかし、引数に他の型が混ざっていた場合は、異なる挙動になってしまいます。

そこでまず、`array_reduce()`に算術演算子を与えたときの挙動を調べ、次に`array_sum()`と`array_product()`の現在の挙動を調べ、最後に不一致を修正するための変更を提案します。


### Behaviour of the array_reduce() variants

算術演算子は数値ではない型の型変換を行いますが、マニュアルでは以下のように記載されています。

:::note
In this context if either operand is a float (or not interpretable as an int), both operands are interpreted as floats, and the result will be a float. Otherwise, the operands will be interpreted as ints, and the result will also be an int. As of PHP 8.0.0, if one of the operands cannot be interpreted a TypeError is thrown.

いずれかのオペランドがfloatであれば両方をfloatとして解釈し、返り値もfloatになります。
それ以外であればオペランドをintとして解釈し、返り値もintになります。
PHP8.0.0以降は、オペランドが解釈できないときにTypeErrorをthrowします。
:::

intとfloat以外の型は、以下のようになります。

・null型は0。
・bool型は、falseは0、trueは1。
・string型は、数値型文字列であればintかfloatになる。
・object型は、do_operationハンドラが実装されていればそれを使う。cast_objectハンドラが実装されていればそれを使う。
・それ以外はすべてTypeError。

### Ecample

以下はGMPによる例です。

```php
$input = [gmp_init(6), gmp_init(3), gmp_init(5)];

// ユーザランド実装
array_reduce($input, fn($carry, $value) => $carry + $value, 0); // object(GMP)#5 (1) { ["num"]=> "14" }
array_reduce($input, fn($carry, $value) => $carry * $value, 1); // object(GMP)#5 (1) { ["num"]=> "90" }

// 正しくない例
$input = [true, STDERR, new stdClass(), [], gmp_init(6)];
array_reduce($input, fn($carry, $value) => $carry + $value, 0); // TypeError
array_reduce($input, fn($carry, $value) => $carry * $value, 1); // TypeError
```

以下はdo_operationハンドラを実装している`FFI\CData`型の例です。

```php
$x = FFI::new("int[2]");
$x[0] = 10;
$x[1] = 25;
$input = [$x, 1];

$output = array_reduce($input, fn($carry, $value) => $carry + $value, 0);
var_dump($output); // object(FFI\CData:int32_t*)#4 (1) { [0] => 25 }
```


### Current behaviour of the array_sum() and array_product() functions

関数の動作は比較的単純です。
まず`array_sum()`は返り値を0に、`array_product()`は1にします。
その後配列を順に処理し、配列かオブジェクトだったら無視し、それ以外なら数値にキャストして戻り値に加えます。

このように、リソースや配列、非数値文字列、加算乗算ができないオブジェクトなどの型を与えた場合でもTypeErrorが発生しません。
通常の算術演算においてはPHP8.0.0で[TypeErrorが発生するようになった](https://qiita.com/rana_kualu/items/fe7998fbe773544d5d25#stricter-type-checks-for-arithmeticbitwise-operators)ため、この動作と矛盾しています。

### Ecample


以下はGMPによる例です。

```php
$input = [gmp_init(6), gmp_init(3), gmp_init(5)];
$output = array_sum($input); // int(0)
$output = array_product($input); // int(1)

// 正しくない例
$input = [true, STDERR, new stdClass(), [], gmp_init(6)];
$output = array_sum($input); // 4
$output = array_product($input); // 3
```

以下はFFIの例です。

```php
$x = FFI::new("int[2]");
$x[0] = 10;
$x[1] = 25;
$input = [$x, 1];

$output = array_sum($input);
var_dump($output); // int(1)
```

## Proposal

このRFCでは、`array_sum()`と`array_product()`について、通常の算術演算と同じ挙動を適用します。
ただし、正しくない値を与えた場合には例外ではなくE_WARNINGを発行し、現在の動作となるべく互換を維持します。

ひとつ注意点として、オブジェクトの加算乗算については数値キャストします。
従って、上記の例は、今後は以下のような動作になります。

GMPの例。

```php
$input = [gmp_init(6), gmp_init(3), gmp_init(5)];

$output = array_sum($input); // 14
$output = array_product($input); // 90
```

正しくない例。

```php
$input = [true, STDERR, new stdClass(), [], gmp_init(6)];

$output = array_sum($input); // 10
// Warning: array_sum(): Addition is not supported on type resource in %s on line %d
// Warning: array_sum(): Addition is not supported on type stdClass in %s on line %d
// Warning: array_sum(): Addition is not supported on type array in %s on line %d

$output = array_product($input); // 18
// Warning: array_product(): Multiplication is not supported on type resource in %s on line %d
// Warning: array_product(): Multiplication is not supported on type stdClass in %s on line %d
// Warning: array_product(): Multiplication is not supported on type array in %s on line %d
```

FFIの例。

```php
$x = FFI::new("int[2]");
$x[0] = 10;
$x[1] = 25;
 
$input = [$x, 1];
 
$output = array_sum($input); // 1
// Warning: array_sum(): Addition is not supported on type FFI\CData in %s on line %d
```


## Backward Incompatible Changes

互換性のない変更点。

不正な型について、E_WARNINGが出るようになります。
オブジェクトを含む場合、異なる結果になる可能性があります。

```php
$a = [10, 15.6, gmp_init(25)];
var_dump(array_sum($a));

// PHP8.2まで 25.6
// PHP8.3以降 50.6
```

## Proposed PHP Version

PHP8.3

## Proposed Voting Choices

投票者の2/3の賛成で受理されます。

投票期間は2023/02/20から2023/03/06です。
本RFCは、賛成27反対0の全会一致で可決されました。


## Implementation

https://github.com/php/php-src/pull/10161


# 感想

ということで、`array_sum|product`が今後はより便利に、というか正常になります。

ただまあ、ひとつ言いたいこととしては、そもそも型を混ぜて演算するんじゃない。