【PHP8.0？】PHPに名前付き引数が実装されるかもしれない

※2020/08/20追記：[PHP8.0で実装されることが決定](https://qiita.com/rana_kualu/items/13bc0c30192ee22c1396)しました。

PHPのソースを眺めていたら、先日2020/04/07にNikitaが[なんか面白そうなプルリクを出して](https://github.com/php/php-src/pull/5357)いました。
[Named Parameters](https://wiki.php.net/rfc/named_params)という2013年に提出されたまま忘れ去られたRFCがあるのですが、その機能を実装したものです。

どういう機能ってこういうのです。

```php
function hoge($foo, $bar){
	echo "foo=$foo,  bar=$bar";
}

hoge(bar=1, foo=2); // foo=2, bar=1
```

[Python](https://qiita.com/junkoda/items/bfd35793c5cd33c600bc)や[CSharp](https://qiita.com/7of9/items/618634ac35a1e27e03eb)なんかで実装されてるやつですね。

Nikita本人は[機能が幾つも不足しているよ](https://github.com/php/php-src/pull/5357#issue-400190970)と言っているのですが、不足の内容はOpcache対応や引数アンパックといった周辺機能で、基本的な機能は既に実装されているみたいです。


# PHP RFC: Named Parameters

## State of this RFC

これは名前付き引数についての準備的なRFCです。
このRFCの目的は、次のPHPバージョンで名前付き引数をサポートすべきか、またサポートするときはどのように実装すべきかを確認することです。
ここで解説している構文や動作は最終的なものではなく、詳細を詰めていく必要があります。

このRFCの実装はまだ完全なものではありません。
これは非常に複雑な機能なので、この機能が必要でなかったのに時間をかけて実装したくはありません。

### Update 22-05-2014

私は他のことで忙しく、このRFCには取り組めていません。
このRFCはPHP6に間に合うように復活させるつもりです。
それまでの間、[未解決の問題に対する議論](http://markmail.org/message/yhtrefvpsndqfm34)の結果の一部をここにまとめておきます。

・名前付き引数のコンセンサスはまだ得られていません。
・名前付き引数のアンパックと、名前のない引数のアンパックは...構文に統合されます。アンパックのRFCは既に決定しているため、ここについて選択肢はあまりありません。
・継承時のパラメータ名チェックは強制されません。これはあまりにも大きなBC breakになるとはんだんされました。

主な未解決の実装上の問題は"Patch"セクションにリストアップされています。

## What are named arguments?

名前付き引数とは、パラメータの順番ではなくパラメータ名を使って関数に引数を渡す方法です。

```php
// 通常の引数
array_fill(0, 100, 42);
// 名前付き引数
array_fill(start_index => 0, num => 100, value => 42);
```

名前付き引数に渡す引数の順番は自由です。
上記例では関数シグネチャと同じ順番で引数を渡していましたが、異なる順番で渡すこともできます。

```php
array_fill(value => 42, num => 100, start_index => 0);
```

名前付き引数と名前のない引数を組み合わせることも可能であり、オプション引数の一部のみを名前付き引数で渡すことも可能になります。

```php
htmlspecialchars($string, double_encode => false);
// ↓と同じ
htmlspecialchars($string, ENT_COMPAT | ENT_HTML401, 'UTF-8', false);
```

## What are the benefits of named arguments?

名前付き引数の明白な利点の一つは、上のコードサンプルを見ればわかります。
変更したい引数までの間にある引数に対して、それぞれデフォルト値を指定する必要から解放されます。
名前付き引数があれば、変更したい引数だけを直接指定することができます。

これは[デフォルト引数のRFC](https://wiki.php.net/rfc/skipparams)でも可能ですが、名前付き引数を使えば意図がより明白になります。
構文を比較してみてください。

```php
htmlspecialchars($string, default, default, false);
// vs
htmlspecialchars($string, double_encode => false);
```

ひとつめのコードを見ても、たまたまhtmlspecialcharsの引数を丸暗記していないかぎり`false`が何を意味するのかは分からないでしょう。

コードの自己文書化の利点は、オプション引数をスキップしないときにおいても明らかです。

```php
$str->contains("foo", true);
// vs
$str->contains("foo", caseInsensitive => true);
```

名前付き引数を使うことで、新しい方法で関数を使うことができるようにbなります。
引数を順番に並んだリストとしてだけではなく、キーと値のペアのリストとしても扱えるということです。
以下のような使い方が考えられます。

```php
// 現在の構文
$db->query(
    'SELECT * from users where firstName = ? AND lastName = ? AND age > ?',
    $firstName, $lastName, $minAge
);
// 名前付き引数
$db->query(
    'SELECT * from users where firstName = :firstName AND lastName = :lastName AND age > :minAge',
    firstName => $firstName, lastName => $lastName, minAge => $minAge
);
```

## Implementation

### Internally

名前付き引数は、他の引数と同じくVM stackを通して渡されます。
これらの引数の違いは、位置引数は常にスタックの先頭に渡されるのに対し、名前付き引数は任意の順番でスタックに挿入することができるということです。
使用されないスタックの位置にはNULLが入り、引数カウントはNULLも数えます。

### Errors

位置引数と名前付き引数を混在させることが可能ですが、名前付き引数は位置引数の後に配置しなければなりません。
そうしなければコンパイルエラーが発生します。

```php
strpos(haystack => "foobar", "bar");
// Fatal error: Cannot pass positional arguments after named arguments
```

可変長引数ではない関数に存在しない引数名を渡した場合、致命的エラーが発生します。

```php
strpos(hasytack => "foobar", needle => "bar");
// Fatal error: Unknown named argument $hasytack
```

同じ名前の引数を複数回渡した場合、新しい方で古い方が上書きされ、警告が発生します。

```php
function test($a, $b) { var_dump($a, $b); }
 
test(1, 1, a => 2); // 2, 1
// Warning: Overwriting already passed parameter 1 ($a)
test(a => 1, b => 1, a => 2); // 2, 1
// Warning: Overwriting already passed parameter 1 ($a)
```

### Collecting unknown named arguments

可変長引数の`...$args`構文を使った場合、余った名前付き引数は`$args`に集められます。
名前付き引数は常に位置引数より後ろとなり、渡された順番が保持されます。

```php
function test(...$args) { var_dump($args); }

test(1, 2, 3, a => 'a', b => 'b');
// [1, 2, 3, "a" => "a", "b" => "b"]
```

使用例としては前述の`$db->query()`があります。

これはPythonで`**kwargs`と呼ばれている機能です。


### Unpacking named arguments

[引数アンパックのRFC](https://wiki.php.net/rfc/argument_unpacking)は名前付き引数のアンパックにも対応します。

```php
$params = ['needle' => 'bar', 'haystack' => 'barfoobar', 'offset' => 3];
strpos(...$params); // int(6)
```

文字列キーを持つ任意の値は、名前付きパラメータとして展開されます。
それ以外のキーは通常の位置引数として扱われます。

位置引数と名前付き引数をひとつの配列にまとめることも可能ですが、その場合でも引数の順番は守らなければなりません。
名前付き引数より後に位置引数が出てきた場合は警告がスローされ、アンパックは中止されます。


### func_* and call_user_func_array

名前付き引数を使って、スタックから引数としてNULLが渡ってきた場合、`func_*`関数の挙動は以下のようになります。

・`func_num_args()`はNULLを含んだ引数の個数を返す。
・`func_get_arg($n)`はデフォルト値を返す。デフォルト値がない場合はNULL。
・`func_get_args()`はデフォルト値を返す。デフォルト値がない場合はNULL。

3関数とも、未定義の引数名は無視します。
`func_get_args`は値を返さず、`func_num_args`はカウントに含めません。

`call_user_func_array`は名前付き引数をサポートしません。
文字列キーを持つ配列を渡すコードが壊れるからです。


## Open questions

### Syntax

現在の実装および提案では、名前付き引数について以下2種類の構文をサポートしています。

```php
test(foo => "oof", bar => "rab");
test("foo" => "oof", "bar" => "rab");
```

ふたつめの構文は、引数名が予約語である場合のためにサポートされています。

```php
test(array => [1, 2, 3]);   // syntax error
test("array" => [1, 2, 3]); // works
```

この構文の選択は恣意的なもので、特に深く考えずに採用しました。
以下に、いくつか代替構文の提案があります(ほとんどはPhil Sturgeonによる提案です)。

```php
// currently implemented:
test(foo => "oof", bar => "rab");
test("foo" => "oof", "bar" => "rab");

// キーワードを使える
test($foo => "oof", $bar => "rab");
test(:foo => "oof", :bar => "rab");
test($foo: "oof", $bar: "rab");

// キーワードを使えない
test(foo = "oof", bar = "rab");
test(foo: "oof", bar: "rab");

// 既に有効なコードなので不可
test($foo = "oof", $bar = "rab");
```

どの構文で決定するかは議論次第です。


### Collection of unknown named args into ...$opts

現在のRFCでは、位置引数と名前付き引数の両方がまとめて可変長引数の`...$opts`に入ってきます。
Pythonでは前者を`*args`に、後者を`**kwargs`に入れるというアプローチをとっています。

Pros：PHPでは、Pythonではできない配列と辞書の混在ができます。
Cons：位置引数と名前付き引数を別にすることで、意図がより明確になります。必ずしも両方の引数をサポートしたいとはかぎらず、片方だけを強制したいかもしれません。

どのように扱うのが適切か、意見や議論を歓迎します。

### Unpacking named args

引数アンパックについても同じ疑問が出てきます。
`...$foo`は位置引数と名前付き引数を一緒にできますが、`*$foo`と`**$foo`に分けるべきでしょうか。

この決定は、可変長引数と同じに揃えるべきでしょう。


### Signature validation allows changing parameter names

現在のところ、引数名はシグネチャに含まれていません。
位置引数しか使わない場合、これは合理的です。
引数名は関数の呼び出しに関係ないからです。

名前付き引数はこの動作を変更します。
継承先クラスが引数名を変更した場合、名前付き引数を使った呼び出しは失敗し、LSPに違反します。

```php
interface A {
	public function test($foo, $bar);
}
 
class B implements A {
	public function test($a, $b) {}
}
 
$obj = new B;
 
// Pass params according to A::test() contract
$obj->test(foo => "foo", bar => "bar"); // ERROR!
```

名前付き引数が導入された場合、シグネチャの検証において引数名の変更にエラーを出さなければなりません。
通常の場合、インターフェイスと実装クラスの不一致は致命的エラーを発生させますが、名前付き引数において致命的エラーを出すとBC breakが大きくなりすぎてしまいます。
かわりに、より低いエラータイプ(WARNING / NOTICE / STRICT)を出すことを検討します。

これに関する具体的な議論のポイントをひとつ挙げておきます。
PHPは、実行時の動作を変更するini設定を導入する習慣を過去に置いてきました。
従って、この挙動をiniで制御できるようにすることは、私の選択肢にはありません。


## Patch

差分が[こちら](https://github.com/nikic/php-src/compare/splat...namedParams)にありますが、このパッチは不完全で、ダーティで、既知のバグがあります。

やるべき作業はまだまだ残っています。

・"Open questions"の結果を実装する。
・内部関数の全てのarginfosをドキュメントと一致するように更新する。現在のarginfo構造体は絶望的に時代遅れで、引数割り当てなどは自動的にできるようにしたい。
・内部関数において引数がスキップされたときに適切に動作するようにする。ほとんどの場合は自動的に動作するはずだが、手動調整が必要になる関数もかなりあるだろう。


# 感想

2014年とかPHP6とか出てくることからわかるように、このRFCはだいぶ昔に書かれてそのまんまです。
当時作成されたパッチはもはや使い物にならないため、新たにプルリクを作ってきたようです。
パッチにしろその他の内容にしろ、当時のPHPと今のPHPはだいぶ異なったものになっているので、なんにしろRFCのリファインは必要になるでしょう。

このプルリクについても、とりあえず提出されただけで何の展開もありませんし、メーリングリストでの議論も特に進んでいるわけではありません。
従って、このプルリクも今後どうなるかはわからず、再びこのまま忘れ去られるかもしれません。
しかしNikitaのことですから、いきなり完動品のプルリクが送られてきて第一線に躍り出る、なんてことがあっても驚きはないですね。


混沌の時代に実装された関数などでは特に、同じ内容の関数でも引数の順番が異なったりしていて大変なのですが、この機能が実装されたら、そのあたりを楽に処理できるようになります。
また[却下されたデフォルト引数のRFC](https://qiita.com/rana_kualu/items/180990033bd9b1b2c316#skipping-optional-parameters)も、この名前付き引数があれば不要になります。
絶対にないといけないというほどでもないですが、存在すれば純粋に便利になる、そんな良い機能だと思います。

あとここからは完全に妄想ですが、このプルリクは[ジェネリクス](https://github.com/nikic/php-src/pull/3)への足掛かりなのではないかと感じています。
Nikitaはどうも[ジェネリクス大好きっ子](https://qiita.com/rana_kualu/items/f75e254553157d8eb0b7)みたいですから、このプルリクを元に引数まわりに手を付けて、ついでにジェネリクスまでできるようにちゃったぜみたいなことを考えているのではないでしょうか。