Quantcast
Channel: PHP7.4タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 113

【PHP7.4】PHPでアロー関数を使えるようになる

$
0
0

かつて、提出されたもののいつのまにか取り下げられていたArrow FunctionsというRFCがありました。

    // $x*2を返す関数
    $mul2 = fn($x) => $x * 2;

    // 使い方
    $mul2(3); // 6

が、なんかV2として復活してました
しかも今回は提案者として重鎮Nikitaが参戦しています。
NikitaはPHPのコア開発者のひとりで、記憶に新しいところではプロパティ型指定を作った人です。

既にコードもできていてプルリクが出されています。

しかしRFCの提出が2019/03/12で、投票は2019/04/17開始・2019/05/01終了です。
なんでそんなスケジュールきつきつなのだ。

RFC Arrow Functions 2.0

Introduction

PHPの無名関数は、単純なことしか行わない場合でもやたら冗長になる場合があります。
使用する変数を毎回手動でインポートしなければならないなど、構文に定型句が多いためです。
このせいで、簡単なクロージャですら読みづらいものとなってしまいます。
このRFCでは、上記の問題に対してより簡潔な構文を提案します。

オーバーヘッドの例として、オンラインで見つけた関数を考えてください。

    function array_values_from_keys($arr, $keys) {
        return array_map(function ($x) use ($arr) { return $arr[$x]; }, $keys);
    }

クロージャが行っている$arr[$x]はたいして難しい内容ではありませんが、定型句のせいで少々わかりづらくなっています。
アロー関数を使うと以下のように書けるようになります。

function array_values_from_keys($arr, $keys) {
    return array_map(fn($x) => $arr[$x], $keys);
}

かつて提出された~>を使うRFCは投票の結果却下されました。
このRFCは、却下されたRFCにおいて持ち上がったいくつかの懸念を解消しています。

このRFCではさらに、様々な代替構文についての詳細な説明も含まれています。
残念ながら、クロージャの短縮構文については、構文や実装に対する制約上の問題で、完璧な解決策を見つけることはできそうにありません。
このRFCでは、その中でも一番ましだと思われる選択をしています。
クロージャ短縮構文の議論は完全に停滞しているため、今後何年も塩漬けしておくよりも、ここで妥協した方がよいでしょう。

Proposal

アロー構文の基本形は以下のようになります。

    fn(parameter_list) => expr

式中で使用されている変数が親スコープ内で定義されている場合、暗黙的に値渡しされます。
次の例では、$f1$f2は同じ動作になります。

$y = 1;

$fn1 = fn($x) => $x + $y;

$fn2 = function ($x) use ($y) {
    return $x + $y;
};

ネストもできます。

$z = 1;
$fn = fn($x) => fn($y) => $x * $y + $z;

外側の関数fn($x)は暗黙的に変数$zを取り込みます。
内側の関数fn($y)も暗黙的に変数$zを取り込みます。
全体として、$zは内側の関数からでも利用可能です。

Function signatures

アロー構文では、引数と戻り値の型、デフォルト値、可変長引数、リファレンスなど、既存の関数と同じ機能を使用できます。
以下は全て有効なアロー関数の構文です。

    fn(array $x) => $x;
    fn(): int => $x;
    fn($x = 42) => $x;
    fn(&$x) => $x;
    fn&($x) => $x;
    fn($x, ...$rest) => $rest;

$this binding and static arrow functions

通常のクロージャ同様に、クラスメソッド中では$this、遅延静的束縛が自動的にバインドされます。
通常のクロージャではstaticキーワードでこれを打ち消すことができるため、アロー関数でも同じ機能をサポートしています。

class Test {
    public function method() {
        $fn = fn() => var_dump($this);
        $fn(); // object(Test)#1 { ... }

        $fn = static fn() => var_dump($this);
        $fn(); // Error: Using $this when not in object context
    }
}

staticクロージャは滅多に使用されません。
これは主に、GCの発生を予測しづらくする$thisのバインドを防ぐために使用されます。
しかし、ほとんどのコードはこのようなことを意識する必要はありません。

この動作について、実際にクロージャ内部で$thisが使用されている場合のみ$thisをバインドするように実装することは可能です。
GCを置いておいても、この変更によって動作が変わることはありません。

残念ながら、PHPは暗黙のうちに$thisを使用することがあります。
たとえば、Fooと互換性のあるスコープからFoo::bar()を呼び出した場合、$thisが継承されます。
我々は内部仕様を知っているため$thisがどうなるか分析できますが、ユーザにとっては$thisがどうなるかは予測不可能です。
そのため、常に$thisを使用する通常のクロージャを使うことを勧めます。

By-value variable binding

既に述べましたが、アロー関数は変数束縛を値渡しで行います。
これは、関数内で使用している全ての変数にuseをすることとほぼ同じです。
従って、値渡しバインディングの値をスコープ内から変更することはできないということになります。

    $x = 1;
    $fn = fn() => $x++; // スコープ外には影響しない
    $fn();
    var_dump($x); // 1

バインディング方法の検討については、discussionセクションを参照してください。

変数の暗黙的な使用と明示的な使用には僅かな違いがあります。
暗黙的な使用では、変数未定義のE_NOTICEは、変数バインド時には発生しません。
すなわち、以下のコードで発生するエラーは、$undefをバインドするときと使用するときの2カ所ではなく、使用するときの一カ所だけです。

    $fn = fn() => $undef;
    $fn();

その理由としては、変数が読み取りのために使用されているのか、参照返り値のために使用されているのかを常に判断することができないからです。
次にやや人為的な例を示します。

$fn = fn($str) => preg_match($regex, $str, $matches) && ($matches[1] % 7 == 0)

この$matchesはpreg_matchによって代入されるので、アロー関数より前に$matchesが存在している必要はありません。
この場合はエラーを出力すべきではありません。

最後に、この自動バインドは文字列リテラルで渡された変数にのみ行われます。
すなわち、以下のコードでは関数内で$xというリテラルが使用されていないため、$xが未定義であるというE_NOTICEが発生します。

    $x = 42;
    $y = 'x';
    $fn = fn() => $$y;

この問題は、使われている変数だけをバインドするのではなく、全ての変数を束縛することによって解消することができます。
不要だと思われるためここでは対応していませんが、それが必要だという声が大きければ変更されるかもしれません。

Precedence

=>の優先順位は最も低くなります。
すなわち、=>の右側は全て=>の中になります。

    fn($x) => $x + $y;

    // ↑と同じ
    fn($x) => ($x + $y);

    // こうではない
    (fn($x) => $x) + $y;

Backward Incompatible Changes

残念ながら、fnはトップレベルのキーワードでなければならず、そしてfnは予約済ではありません。

Ilija ToviloがGitHubのトップ1000リポジトリを調査してfnの使用を調査しました。
概要として、fnの使用は全てテスト内であるか、名前空間内での使用でした。

Examples

以下の例は旧RFCからのコピペです。

silexphp/Pimple

// 現在
$extended = function ($c) use ($callable, $factory) {
    return $callable($factory($c), $c);
};

// アロー関数
$extended = fn($c) => $callable($factory($c), $c);

定型文が44文字から8文字に減りました。

Doctrine/DBAL

// 現在
$this->existingSchemaPaths = array_filter($paths, function ($v) use ($names) {
    return in_array($v, $names);
});

// アロー関数
$this->existingSchemaPaths = array_filter($paths, fn($v) => in_array($v, $names));

31文字から8文字に減りました。

多くのライブラリで見られる関数も、以下のように書けるようになります。

// 現在
function complement(callable $f) {
    return function (...$args) use ($f) {
        return !$f(...$args);
    };
}

// アロー関数
function complement(callable $f) {
    return fn(...$args) => !$f(...$args);
}

// 現在
$result = Collection::from([1, 2])
    ->map(function ($v) {
        return $v * 2;
    })
    ->reduce(function ($tmp, $v) {
        return $tmp + $v;
    }, 0);

echo $result; // 6

// アロー関数
$result = Collection::from([1, 2])
    ->map(fn($v) => $v * 2)
    ->reduce(fn($tmp, $v) => $tmp + $v, 0);

echo $result; // 6

Discussion

Syntax

おそらく最も望ましいアロー関数の構文は($x) => $x * $y、もしくは$x => $x * $yです。
非常に簡潔で、JavaScriptなどの多くの言語で使用されています。
しかしPHPにおいてこの構文は問題が多く、使用することは困難です。
このセクションではアロー関数のために考慮されたいくつかの構文について、利点と欠点を解説します。

($x) => $x * $y

最も一般的であり、そしてPHPでは不可能な構文です。
この構文最大の問題が、配列やyieldなどにおいてキーと値のペアを指定する目的で=>が既に使用されていることです。
従って、以下のコードの=>は配列宣言なのか、アロー関数なのかを特定することができません。

$array = [
    $a => $a + $b,
    $x => $x * $y,
];

しかし、この曖昧さ自体は問題ではありません。
式の構文は既に曖昧さに満ちていますが、優先順位、結合度、あるいはその他の規則によって一意に解決できます。
今回の場合は、後方互換性を保つため、上記の構文は配列宣言であり、アロー関数は以下のようにする、と定義すれば一意にすることはできます。

$array = [
    ($a => $a + $b),
    ($x => $x * $y),
];

同じ問題がyieldにも存在します。

yield $foo => $bar; // 配列
yield ($foo => $bar); // アロー関数

実はアロー関数がなかったとしても、既に解釈が曖昧になる構文は存在します。

$array = [yield $k => $v];
// こうなる
$array = [(yield $k => $v)];
// こう解釈することもできる
$array = [(yield $k) => $v];

$x => $yには、さらに次のセクションで解説する問題もあり、そして実はそちらが致命傷です。

($x) ==> $x * $y

Hackが使用している($x) ==> $x * $y、以前のRFCで提出された($x) ~> $x * $y、あるいは類似の構文についてです。
($x, $y) ==> $x + $yのような単純な構文であればサポートも可能ですが、==>の左側に任意の構造を記述できるようにすると、パーサの実装が非常に困難になります。

($x = [42] + ["foobar"]) ==> $x; // 代入式
(Type &$x) ==> $x;               // 定数とビット積

$a ? ($b): Type ==> $c : $d;

($a = ($a = ($a = ($a = ($a = 42) ))))
($a = ($a = ($a = ($a = ($a = 42) ==> $a))))

以下構文のパース処理がとってもたいへんで、解決するにはパーサを入れ替えるレベルの改修が必要だ的な記述が延々書かれているのですが、細かすぎてよくわからないので省略。

fn($x) => $x * $y

上記の構文候補に関する最大の問題は、(の対となる==>が見つかるまではアロー関数であるかそうでないかを調べ続けなければならない、ということでした。
明確な解決策は、特有の先行記号を持つように構文を修正することです。
このRFCでは、短くて読みやすい候補としてfnを提案しています。
問題点は、fnが予約語ではないことです。

先行記号のシンボルについては、もちろん他の候補もあります。
特に未使用の単項演算子というパンドラの箱を開けたら。

function($x) => $x * $y
fn($x) => $x * $y
\($x) => $x * $y
^($x) => $x * $y

*($x) => $x * $y
$($x) => $x * $y
%($x) => $x * $y
&($x) => $x * $y
=($x)=> $x * $y

// NG PHPとして正しい構文である
!($x) => $x * $y
+($x) => $x * $y
-($x) => $x * $y
~($x) => $x * $y
@($x) => $x * $y

// NG _は正しい関数名である
_($x) => $x * $y

実際に実現可能であろう文法は最初の4例でしょう。

fnは今回提案されているものです。

fucntionは既にキーワードとして使用されているので安全です。
不利な点はキーワードが長いということで、そしてアロー関数のセールスポイントのひとつが短いということです。

\($x) => $x * $yはHaskellのラムダ構文と類似の例で\はプアマンズλです。

^はC言語でサポートされています。

先頭に記号が付いた構文を使うとなれば、=>も邪魔なので消したくなります。
fn($x) ⇒ $x * $yのかわりにfn($x) $x * $yとは書けないでしょうか?
残念ながら、返り値が曖昧になるためこれは不可能です。

    fn($x): T \T \T
    // こう?
    fn($x): T\T (\T)
    // それともこう?
    fn($x): T (\T\T)

名前空間の空白サポートをやめ、名前空間は単一トークンと字句解析すれば、この曖昧さは解決可能です。
しかし、それは破壊的変更になってしまいます。

Using -> and --> as arrows

=>のかわりに->-->を使おうという提案がありました。
しかしこれらは別の既存構文と衝突します。
->はプロパティアクセスで既に使用されています。

($x) -> $x
// 正しい構文。↓と同じ
$x->{$x}

-->-->の組み合わせです。

$x --> $x
// 正しい構文。↓と同じ
$x-- > $x

必ず括弧を使うと定義した場合、-->は使用可能です。
($x)--は現在のところ正しい構文ではないからです。

いずれの記号も先行記号と組み合わせれば使用可能ですが、そうするのであれば=>でも同じことです。

Different parameter list separators

Rustなどいくつかの言語は、クロージャのパラメータに異なる種類のセパレータを使います。

|$x| => $x * $y

|は有効な単項演算子ではないため、先行記号と同じ区別に使用できます。
しかし残念ながら、|はUnion typesやビット演算子、デフォルト値などで使用されています。

|T1|T2 $x = A|B| => $x

構文上の曖昧さはないと思われますが、意味を読み取るのは困難です。
また|$x|のような構文はPHPとしては異質でしょう。

Block-based syntax

これまでに解説したものとは全く異なるアプローチとして、RubyやSwiftで使用されているブロックベース構文があります。
考えられる構文は以下のようなものです。

{ ($x) => $x + $y }

先頭に{がありますが、PHPは独立ブロックとしての{の使用をサポートしているので、先行記号としては使えません。
以下は正当なPHPコードです。

{ ($x) + $y };

すなわち、構文解析の発散問題がやはり発生することを意味します。
ただし、この場合は簡単な回避策があります。
すなわち、式文としてのクロージャ構文を禁止することです。
単独のクロージャは許可されず、何らかの式の一部である必要があるということにします。

{ ($x) => $x + $y }; // NG
$fn = { ($x) => $x + $y }; // OK

これでブロックベース構文は汎用的に実行可能になります。
個人的には、これがfn()より優れているとは思えず、特にアロー関数をネストする場合はさらにややこしくなります。

fn($x) => fn($y) => $x * $y
{ ($x) => { ($y) => $x * $y } }

C++ syntax

C++11では、以下の構文でラムダを使用可能です。

cpp
[captures](params){body}

PHPの場合は[$x]($y)が既に有効な構文であるため、この構文は使用できません。

Miscellaneous

Haskellの構文に近い、パラメータを括弧で囲まない\param_list => expr構文も考えられました。
が、PHPではこの構文は曖昧です。

[\T &$x => $y]
// こう?
[\(T &$x) => $y)]
// それともこう?
[(\T & $x) => $y]

Binding behavior

アロー関数に関するもうひとつの議論点はバインディングです。

アロー関数は親スコープに存在する変数を自動的にバインドします。
問題はそのバインドをどのように動作させるべきかです。
基本的に3つの選択肢があり、それぞれを値・参照・変数によるバインディングとします。

$x = 1;
$fn = fn() => $x++;
$fn();
var_dump($x); // 値渡しなら1
              // 参照渡しなら2

値によるバインドはuse ($x)に、参照によるバインドはuse (&$x)に相当します。
参照バインディングの利点はアロー関数内で変数を変更できることです。

残念ながら、2つの大きな問題のため、参照バインディングが値バインディングより優れているとは言えません。
ひとつめは、参照バインディングのためには参照ラッパーの作成と参照が必要となるため、パフォーマンスが低下するということです。
アロー関数と通常クロージャを選ぶときの基準が性能差であるとなれば、それはとても残念なことです。

もうひとつ重要な問題が、クロージャの内側から外側の変数を変更することができるのと同時に、クロージャの内側の変数を外側から変更することもできるということです

次の例では、なぜこれが問題になるのか、暗黙の参照バインディングがいかに直感的でない結果になるのかを説明します。

$range = range(1, 5);
$fns = [];
foreach ($range as $i) {
    $fns[] = fn() => $i;
}
foreach ($fns as $fn) {
    echo $fn();
}
// 値:   1 2 3 4 5
// 参照: 5 5 5 5 5
// 変数: 5 5 5 5 5

アロー関数が値バインドである場合、全て正常に動作します。

参照バインドの場合、以下のように動作します。
最初のループでクロージャ内の$iがforeach内の$iへの参照にバインドされます。
2回目のループで、この参照先の値が上書きされ、さらに新しいクロージャで$iにバインドされます。
ループが終了した後、全てのクロージャはひとつの参照を共有します。
そして値は最後に割り当てられた値です。

今回は議論されておらず、PHPでは使用されていない3番目のバインドが、変数バインドです。
これこそが本当のスコープバインディングで、クロージャ外の変数とクロージャ内の変数は共有されています。
これは参照バインドと同じように見えますが、次の例が示すように全く同じではありません。

$range = range(1, 5);
$fns = [];
foreach ($range as &$i) { // &を追加
    $fns[] = fn() => $i;
}
foreach ($fns as $fn) {
    echo $fn();
}
// 値:   1 2 3 4 5
// 参照: 1 2 3 4 5
// 変数: 5 5 5 5 5

参照バインドをリファレンス付きでforeachすると動作が変わります。
参照によるforeachは、値の代入ではなく、参照の代入を実行します。
これにより、前ループの参照関係は解除されます。
すなわち、各クロージャは対応する配列要素を参照する個々の参照を取得することになります。

変数バインドを使用した場合、foreachの呼び出し方は問題になりません。
外側の$iとクロージャ内の$iは文字どおり完全に同じなので、クロージャが呼び出された時点の最終的な$iの値だけが意味を持ちます。

変数バインドはPHPでは実装が難しく、値バインドと同程度の性能を出すことは不可能かもしれません。

このような問題があるため、PHPで実装可能な唯一のバインディング形式は値バインドであると私は考えています。
ただし、特に下記のブロックベース構文がサポートされたような場合は、以下のように明示的に参照バインドを許可すると有用かもしれません。

$fn = fn() use(&) {
    // ...
};

Future Scope

将来は以下のように拡張される可能性がありますが、必ずしも推奨されるわけではありません。

Multi-statement bodies

このRFCでは、アロー関数は暗黙的に返される式をひとつだけしか持つことができません。
しかし、他の言語ではブロック形式で複数の式を受け入れるのが一般的です。

fn(params) {
    stmt1;
    stmt2;
    return expr;
}

この構文は優先価値が低いため、このRFCでは省略されています。
この構文をサポートする利点は、単一の式を使用するか複数のステートメントを使用するかによって2つの異なる構文を使い分けるのではなく、常に単一のクロージャ構文を使用できるようになることです。

Switching the binding mode

デフォルトでは値バインドを使用しますが、参照バインドもできるように構文を拡張することもできます。
複数ステートメントを使用する際の本文は、外側の変数を変更することに興味がある可能性が高いので、Multi-statement bodiesと組み合わせると特に役立つでしょう。
構文は以下のようになると思われます。

$a = 1;
$fn = fn() use(&) {
    $a++;
};
$fn();
var_dump($a); // int(2)

あるいは、基本的に値バインドを維持し、参照バインドしたい変数は明示することです。

$a = 1;
$b = 2;
$fn = fn() use(&$a) {
    $a += $b;
};
$fn();
var_dump($a); // int(3)

この例では$bは値バインドであり、$aは参照バインドとなります。
ただし、この構文は既存のクロージャ構文に近いため、混乱を招く可能性があります。
通常クロージャ構文では$bはバインドされません。

Allow arrow notation for real functions

通常の関数やメソッドにもアロー関数を使用できるようにするのもよいかもしれません。
getterのような単一定型文を減らすことができるでしょう。

class Test {
    private $foo;
    private $bar;

    fn getFoo() => $this->foo;
    fn getBar() => $this->bar;
}

対象バージョン

PHP7.4です。
7.4の新機能多すぎるんだけど大丈夫なのこれ。

投票

2019/04/17に投票開始、2019/05/01に投票終了。
有権者の2/3+1の賛成で受理されます。

2019/04/22時点では賛成36、反対7で、よほどの問題でも発生しないかぎりPHP7.4で導入されます。

感想

JavaScriptのアロー関数って個人的に好きじゃないんですよね。

JavaScript
const hoge = () => a++;

let a = 1;
hoge();
a; // 2 ←

普段スコープがー副作用がーとか言ってるわりに、どうしてこれが平気なのか理解に苦しむ。

PHPのアロー関数は上記のとおり値バインドであり、このような問題は発生しません。

PHP
$hoge = fn() => $a++;

$a = 1;
$hoge();
echo $a; // 1

しかし、そういう意思を持った上で値バインドを選んだ、というわけではなく単に実装上の理由というのは少々もにょる。

また、これまでのPHPの文法とはかけ離れた異質な構文なので、慣れないと扱いづらそうです。
特にPHPの場合、=>は既に別の構文で使用されています。
パーサによるパースに対しては=>を使っていても誤解が生まれないようになっているのですが、しかし人間によるパースが誤解しやすいことは間違いありません。
どうせ既存構文とは全く互換性がないのだから、人力パースにも優しい新たな演算子を設けてほしかったところですね。

あと全く関係ないのだけど、名前空間にスペース空けられるってのをこのRFCで初めて知ったよ。

namespace A                 \B;
    echo __NAMESPACE__; // A\B

Viewing all articles
Browse latest Browse all 113

Trending Articles