【PHP8.5】返り値を無視することを許さないNoDiscardアトリビュート


PHPは元々型や引数まわりのシステムがアバウトだったこともあり、返り値を受け取らずに放棄する書き方がまかり通っていることがよくあります。

```php
$fp = fopen('/tmp/lock.txt', 'r+');
flock($fp); // うごく
fwrite($fp, $text);
flock($fp, LOCK_UN);
fclose($fp);
```

まあPHP以外の言語でも普通に可能ですが。

ということで、帰り値の受け取りを強制するアトリビュート`#[\NoDiscard]`が提唱されました。
既に受理されており、PHP8.5から使用可能になります。


# [PHP RFC: Marking return values as important (#[\NoDiscard])](https://wiki.php.net/rfc/marking_return_value_as_important)

## Introduction

現代のPHP APIで採用されるエラー処理メカニズムは例外です。
このメカニズムでは、失敗したときの処理を行わないかぎり非常に明白なエラーが発生します。
しかし、全てが成功もしくは失敗だけのどちらかだけではなく、部分的に成功というステータスを取りたい場合もあるでしょう。

この場合は返り値によって対応することができます。
しかしその場合、返り値をチェックしなければサイレントなエラーが起きてしまう可能性もあります。
APIによっては滅多にエラーが起きなかったり、非決定的な場合があるため、一見しただけでは問題に気付かないことがあります。

```php
/**
 * 配列の全値を処理して返す。
 * 返り値は成功した項目はnull、失敗なら例外の入った配列。
 * 
 * @param array<string> $items
 * @return array<null|Exception>
 */
#[\NoDiscard("as processing might fail for individual items")]
function bulk_process(array $items): array {
	$results = [];
 
	foreach ($items as $key => $item) {
		if (\random_int(0, 9999) < 9999) {
			// 99.99%は成功するダミー処理
			echo "Processing {$item}", PHP_EOL;
			$error = null;
		} else {
			$error = new \Exception("Failed to process {$item}.");
		}
 
		$results[$key] = $error;
	}
 
	return $results;
}
```


## Proposal

このRFCでは、関数およびメソッドの返り値が重要であり、返り値に何もしないことはバグであるとみなすアトリビュート`#[\NoDiscard]`を導入します。
以下、関数と言った場合は関数およびメソッドを意味します。

```php
#[Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_FUNCTION)]
final class NoDiscard
{
    public readonly ?string $message;
 
    public function __construct(?string $message = null) {}
}
```

`#[\NoDiscard]`のついた関数を呼び出す場合、エンジンは返り値が使用されていないときにはE_WARNINGもしくはE_USER_WARNINGを発行します。

『返り値を使用する』とは、返り値が他の式の一部であることを意味します。
意味のある式である必要はなく、ダミー変数に割り当てるだけでも使用したとみなされます。

### (void) cast to suppress the warning

ただし、OPCacheは意味のない構文を最適化に伴って除去する可能性があります。
これによって警告が出てしまう可能性があります。
そこで本RFCは、返り値を意図的に使用しないことを表す`void()`キャストも提案します。
`void()`キャストは文であり、式ではありません。
式中で使用すると構文エラーが発生します。

### Native functions where the attribute is applied

このRFCでは、以下のネイティブ関数にアトリビュートが適用されます。

・[flock](https://www.php.net/manual/ja/function.flock.php)
ロックを取得できなかったことを無視すると、データの破損が発生する可能性があります。
ロック失敗は負荷が高いときにのみ発生するので、テストで問題を発見することは困難です。

・[DateTimeImmutable::set*()](https://www.php.net/manual/ja/datetimeimmutable.setdate.php)
そもそも名前が不適切であり、オブジェクト自体の値は更新されることはなく、新しいインスタンスを返します。
この種のメソッドに確立された命名スキームは`with*()`です。
この命名では、DateTimeからDateTimeImmutableに移行した際に返り値の使用を忘れる可能性があります。

### Constraints

`#[\NoDiscard]`を追加すると、以下の場合にコンパイルエラーが発生します。

・関数の返り値の型として`:void`や`:never`が指定されている
・返り値がないマジックメソッド。`__construct`と`__clone`。


## Examples

```php
$items = [ 'foo', 'bar', 'baz' ];
 
// Warning: The return value of function bulk_process() is expected to be consumed, as processing might fail for individual items
bulk_process($items);
 
// エラー出ない。
$results = bulk_process($items);

// エラー出ない。
(void)bulk_process($items);
 
// エラー出ない。ただしOPCacheによって除去される可能性があり、その場合はエラーが出る。
(bool)bulk_process($items);
```


```php
set_error_handler(static function (int $errno, string $errstr, ?string $errfile = null, ?int $errline = null) {
	throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
});

#[\NoDiscard("エラーメッセージ")]
function bulk_process(array $items): array {
	echo __FUNCTION__, PHP_EOL;
 
	return [];
}
 
function get_items(): array {
	echo __FUNCTION__, PHP_EOL;
 
	return [ 'foo', 'bar', 'baz' ];
}
 
try {
	// "get_items"
	bulk_process(get_items());
} catch (\Exception $e) {
	// "The return value of function bulk_process() is expected to be consumed, as エラーメッセージ"
	echo $e->getMessage(), PHP_EOL;
}
```


```php
#[\NoDiscard]
function bulk_process(array $items): void { }  // Fatal errorになる。返り値はvoid

#[\NoDiscard]
function bulk_process(array $items): never { }  // Fatal errorになる。返り値はnever
```

### Recommended usage

このアトリビュートは、関数のユーザが返り値の使用を忘れる可能性があり、返り値を使用しないと発見することが難しい問題の発生する可能性がある関数で使用することを目的としています。

上記の`bulk_process()`は適切な使用例です。
これは副作用のある関数であり、大抵は成功するので、普通のテストでは失敗例を見逃しやすくなります。

いっぽう`str_contains()`に使うのは不適切です。
開発者が`str_contains()`を使ったうえで結果に対して何もしないことは考えられません。
また本関数には副作用がなく、返り値の使用を忘れたときは単に不要な処理が行われるだけです。
同様に、ほとんどの純粋関数には必要ありません。


## Precedent

先例。

### PHP

PHPStorm IDE、PHPStan、Psalmは、純粋関数の返り値が使用されていないチェックをすでにサポートしており、`DateTimeImmutable::set*()`の落とし穴を検出可能です。
ただし`bulk_process()`に対しても検出できる診断は、RFC作成者の知るかぎりではありません。


### Other programming languages

この機能と`(void)`キャストについては、[C言語](https://en.cppreference.com/w/c/language/attributes/nodiscard)と[C++言語](https://en.cppreference.com/w/cpp/language/attributes/nodiscard)からインスパイアされています。

[Rust](https://doc.rust-lang.org/reference/attributes/diagnostics.html#the-must_use-attribute)は`#[must_use = "message"]`構文をサポートしており、メッセージをサポートするきっかけになりました。

Javaは一部のエコシステムに`@CheckReturnValue`アノテーションがありますが、言語自体にはありません。

Swiftはデフォルトで警告を発します。
`@discardableResult`アトリビュートで、警告を抑えることができます。

Golangにはデフォルトで警告を発するようにする[機能リクエスト](https://github.com/golang/go/issues/20803)が存在します。

TypeScriptには[機能リクエスト](https://github.com/microsoft/TypeScript/issues/8240)が存在します。
ESLintには[Promiseが消化されているかチェックする機能](https://typescript-eslint.io/rules/no-floating-promises/)が存在します。


## Backward Incompatible Changes

グローバル名前空間でクラス名`NoDiscard`が使用できなくなります。
[GitHub上には存在せず](https://github.com/search?q=language%3APHP%20symbol%3ANoDiscard&type=code)、影響はほぼありません。

`(void)`キャストは現在、`(void)`という名前の定数への有効なアクセスです。
GitHub上には存在せず、影響はほぼありません。

一部の関数は、安全でない方法で使用するとE_WARNINGを発生するようになります。
ダミー変数に割り当てることでエラーを抑制する回避方法は、古いバージョンのPHPでも確実に利用可能であるため、利点が互換性の問題点を上回ると考えています。


## Proposed PHP Version(s)

PHP8.5

## RFC Impact

### To Opcache

新しいセマンティクスが追加されるため、OPCacheの変更が必要になります。
現在の実装は、JITを含めてすべてのテストを通過しています。

### New Constants

`ext/tokenizer`に定数`T_VOID_CAST`が追加されます。


## Future Scope

将来の展望は、今のところありません。

## Proposed Voting Choices

可決には2/3の賛成が必要です。

本提案は、賛成18反対4の賛成多数で受理されました。


## Patches and Tests

・[#[\NoDiscard] attribute](https://github.com/php/php-src/pull/17599)
・[(void) cast](https://github.com/php/php-src/pull/18115)


# 感想

ユーザへの影響としては、[flock](https://www.php.net/manual/ja/function.flock.php)が一番大きいでしょう。
まあ対処も`$_ = flock()`ってするだけだから一瞬ですが。

自分で使うときは、どのようなときに使えばいいのかいまいちよくわかりません。
きっと誰かがよい使い方を教えてくれるはず。