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])
Introduction
現代のPHP APIで採用されるエラー処理メカニズムは例外です。
このメカニズムでは、失敗したときの処理を行わないかぎり非常に明白なエラーが発生します。
しかし、全てが成功もしくは失敗だけのどちらかだけではなく、部分的に成功というステータスを取りたい場合もあるでしょう。
この場合は返り値によって対応することができます。
しかしその場合、返り値をチェックしなければサイレントなエラーが起きてしまう可能性もあります。
APIによっては滅多にエラーが起きなかったり、非決定的な場合があるため、一見しただけでは問題に気付かないことがあります。
/**
* 配列の全値を処理して返す。
* 返り値は成功した項目は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]
を導入します。
以下、関数と言った場合は関数およびメソッドを意味します。
#[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
ロックを取得できなかったことを無視すると、データの破損が発生する可能性があります。
ロック失敗は負荷が高いときにのみ発生するので、テストで問題を発見することは困難です。
・DateTimeImmutable::set*()
そもそも名前が不適切であり、オブジェクト自体の値は更新されることはなく、新しいインスタンスを返します。
この種のメソッドに確立された命名スキームはwith*()
です。
この命名では、DateTimeからDateTimeImmutableに移行した際に返り値の使用を忘れる可能性があります。
Constraints
#[\NoDiscard]
を追加すると、以下の場合にコンパイルエラーが発生します。
・関数の返り値の型として:void
や:never
が指定されている
・返り値がないマジックメソッド。__construct
と__clone
。
Examples
$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);
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;
}
#[\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言語とC++言語からインスパイアされています。
Rustは#[must_use = "message"]
構文をサポートしており、メッセージをサポートするきっかけになりました。
Javaは一部のエコシステムに@CheckReturnValue
アノテーションがありますが、言語自体にはありません。
Swiftはデフォルトで警告を発します。
@discardableResult
アトリビュートで、警告を抑えることができます。
Golangにはデフォルトで警告を発するようにする機能リクエストが存在します。
TypeScriptには機能リクエストが存在します。
ESLintにはPromiseが消化されているかチェックする機能が存在します。
Backward Incompatible Changes
グローバル名前空間でクラス名NoDiscard
が使用できなくなります。
GitHub上には存在せず、影響はほぼありません。
(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
・(void) cast
感想
ユーザへの影響としては、flockが一番大きいでしょう。
まあ対処も$_ = flock()
ってするだけだから一瞬ですが。
自分で使うときは、どのようなときに使えばいいのかいまいちよくわかりません。
きっと誰かがよい使い方を教えてくれるはず。