166
145

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PHPの無名関数式とは何か、そしてPHP7.4のアロー関数にそなえよう

Last updated at Posted at 2019-08-04

PHP 7.4は現在beta1までリリースされたので、今頃はみなさまも12月のPHP 7.4.0正式リリースに向けて遊び倒してるところだと存じます。7.4でPHPに新たに導入される文法はいくつかありますが、その目玉のひとつがArrow Functions 2.0です。

ちなみにこの記事は「アロー関数はすべての無名関数式の代替になるわけではない」として書きはじめたので、今回本当に書きたかったのはそのあたりです。

最初にまとめると、「アロー関数は簡単なことを短く書ける」記法です。
複雑なことをするには既存のfunction式の方が適してます

前提知識: 式と文

PHPをはじめとするC言語に影響を受けた多くのプログラミング言語では「 (expression)」と「 (statement)」を厳密に区別する特徴があります。

雑に言うと、文とは以下のようなものです。

  • 制御構造
    • if, else, elseif
    • foreach
    • for
    • while, do
    • switch, case
    • try, catch, finally
    • ラベル
    • goto
    • return
    • throw
  • 定義文
    • function (式もある)
    • class (式もある)
    • trait
    • interface
  • 宣言文
    • use
    • declare
    • namespace
    • const
    • global
    • var
    • public, protected, private
    • static
    • __halt_compiler()
  • その他の構文
    • echo, print
      • これらはPHPの構文上、関数とは明確に区別されます
      • printは書きかたによって式に含めることもできます

これらの文の分類は私が大雑把に分けたもので、公式の定義ではありません。

それ以外の言語要素は基本的に式です。また、式同士は構文や演算子によって結合して式になります。

  • 関数呼び出し
    • var_dump($arg)
    • $obj->m($arg)
    • Klass::m($arg)
    • vendor\ns\f($arg)
  • 単項演算子
    • !cond()
    • +123, -123
    • ~$bit
    • @foo()
    • $n++, $n--, ++$n, --$n
  • 演算子式
    • 比較式
      • $a < $b, $a > $b, $a <= $b, $a <= $b
      • $a == $b, $a != $b, $a === $b, $a !== $b, $a <> $b
      • $a <=> $b 宇宙船演算子
      • $obj instanceof Klass 型演算子
    • 算術演算
      • $a + $b, $a - $b, $a * $b, $a / $b, $a % $b, $a ** $b
    • ビット演算
      • $a & $b, $a | $b
      • $bit << $n, $bit >> $n
    • 文字列結合
      • $str . "ing"
    • 条件演算子
      • $var ? 'foo' : bar()
      • $var ?: func() エルビス演算子
      • $var ?? 'default' null合体演算子
    • 代入
      • $var = 1
      • list($a, $b) = foo(), [$a, $b] = $foo(), list('a' => $a, 'b' => $b) = ['a' => 1, 'b' => 2]
      • $a += $b, $a -= $b, $a *= $b, $a /= $b, $a %= $b
    • $var && func()
  • リテラル・変数
    • $var, ${var}, $$varname, ${'var'}
    • "{$var}", "$(var)"
    • [1, 2, 3], ['key' => $val], array(1, 2, 3), array('key' => $val)
  • その他の言語構造
    • (int)$var キャスト
    • $arr['key'] 配列アクセス
    • $obj->prop プロパティアクセス
    • Klass::CONST_VALUE クラス定数
    • isset($var)
    • empty($var)
    • assert($cond)
    • exit(0), die(1)
    • yield $var
    • include, require, include_once, require_once
    • print("message")
      • printは式の一部として書ける記法と文でしか成立しない記法の両方があるので特殊
    • new Klass, new Klass($arg), new $class
    • new class{ ... }

などです。寝惚けた頭で適当に列挙しただけなので、漏れがありそうですね。

0個以上の式の組み合せから構成される文を「式文(expression statement)」と呼びます。式文は;または?>で終端します。

以下の;で終端する行はすべて式文です。

require_once __DIR__ . '/Foo.php';
$config = include(__DIR__ . '/config.php');
$var = 1;
f();
$a = [1, 2, ...y()];
$b = array_merge($a, z());
$c = $options + ['foo' => true];
$obj->method($a, $b, 1);
throw new LogicException('Aieee')
$foo === 1 or die(1);
assert($hoge instaceof Fuga);
exit;
print(1);

原則として、式と直接組み合せられるのは式だけです。

また、文の中にも式しか書けない箇所がいくつもあります。

if ($cond) {
   // ...
}

foreach (foo() as $f) {
    // ...
}

switch ($value)
{
    // ...
}

これらの文の $condfoo(), $value の場所には式しか書くことができません。翻せば、式であれば何でも書いてよいということです。

関数式とは何か

関数定義文の制約

通常の関数定義文は以下のような構造をとります。

function x()
{
    var_dump('Called x()');
}

この文はグローバル名前空間に x() という関数として定義されます。

次に、 x() の中でローカルに使いたい関数を用意したくなったとします。

function x()
{
    function _inner()
    {
        var_dump('Called inner()');
    }

    _inner();
    _inner();
    _inner();
}

x();
// 'Called inner()' が三回出力される

実はこの文は有効です。しかし、いくつかの問題があります。

ひとつは、 _inner()x() の外からも呼べることです。PHPのfunction文は実はさまざまな箇所に記述することが文法上許されるのですが、メソッド定義の文脈を除いて、「現在の名前空間に関数を定義する」機能を持つだけです。メソッド定義の文脈とは、クラス定義(class, interface, traitを含む)の直下のことです。

つまり、 _inner() は「x()の中だけで使えるローカル関数」ではなく、PHPではただのグローバル関数定義です1

次に、PHPでは原則として関数の再定義はできない制約があることです。先ほどのコードは x() を一回しか起動してないので一見して動作するように見えましたが、x()を2回呼び出すと Fatal error: Cannot redeclare _inner() というエラーが発生するはずです。

PHPの構文では以下のような記述も許されますが、実際にはまったくの無価値です。

for ($i = 0; $i == 0; $i++) {
    function x()
    {
        var_dump("called x()");
    }
}

関数定義文の問題は、関数を作成するときに必ず名前がついてしまうので複数回定義できないことです。また、変数によって関数名や定義内容を動的に変化させることも基本的にはできません2

function関数式

関数式は名前をつけずにローカルな関数を作成できる言語機能です。ここで作った関数はClosureクラスのオブジェクトなので、変数に代入することも、関数に渡すこともできます。この関数にはfunction文で定義された関数のような名前がないので「無名関数」とも呼ばれます。

これはPHP以外のプログラミング言語で「クロージャ」「ラムダ式」と呼ばれるものと同じ概念です。

function x()
{
    $inner = function () {
        var_dump('Called inner()');
    };

    $inner();
    $inner();
    $inner();
}

x();
// 'Called inner()' が三回出力される

変数のキャプチャ (クロージャ)

この関数式は単にローカルで使用するための関数として使うこともできるのですが、もっとおもしろい特徴があります。それは変数を「キャプチャ」(捕捉)できることです。

わかりやすい例として、「常に1を返す関数」「常に2を返す関数」「常に3を返す関数」…「常にnを返す関数」 を数多く作ることができます。

$f = [];

foreach (range(0, 10) as $n) {
    $f[$n] = function () use ($n) {
        return $n;
    };
}

var_dump($f[0]()); // int(0) が表示される
var_dump($f[1]()); // int(1) が表示される
var_dump($f[2]()); // int(2) が表示される
var_dump($f[3]()); // int(3) が表示される
// ...

ここでは use が肝です。この場合は $n の変数参照ではなく、値がコピーされて捕捉されます。逆に言うと、「名前付きで定義されない」「変数をキャプチャできる」以外は通常の関数と同じことができます。

この「変数を捕捉できる」という特徴を活かすと、とてもおもしろい使いかたができます。

// キャラクターのせりふを画面に出力する
$character = function ($name) {
    return function ($script) use ($name) {
        echo "{$name}{$script}\n";
    };
};

$taro = $character("太郎");
$jiro = $character("二郎");

$taro("こんにちは、初めまして");
$jiro("どうも、私は二郎です");

$characterは、最初に受け取った変数を閉じ込めた新しい関数を作って返します。このような特徴から「PHPの無名関数はクロージャ(関数閉包)である」と説明でき、それはClosureというクラス名にも反映されています。

クロージャと状態の更新

無名関数は変数を捕捉できますが、デフォルトでは「値をコピーして捕捉」するため、変更することはできません。しかし、リファレンスを組み合せることで変更も可能です。

この仕様を利用すれば、クロージャの例題としてしばしば例に出されるカウンターも実装できます。

function make_counter(int $n = 0)
{
    return function ($add = 1) use (&$n) {
        return $n += $add;
    };
}
// この関数は $make_counter = function (){...}; で定義してもよい

$c = make_counter(10);
var_dump($c(2));
var_dump($c(2));
var_dump($c(2));
var_dump($c(5));

無名関数と再帰

通常の関数定義文であれば問題なく実行できる関数の再帰呼び出しですが、無名関数では制約があります。

function fib($n)
{
    return ($n < 2) ? 1 : fib($n - 1) + fib($n -2);
}
// 問題なく再帰できる

ここまで説明したように変数リファレンスのキャプチャを利用することで自己再帰できます。

$fib = function ($n) use (&$fib) {
    return ($n < 2) ? 1 : $fib($n - 1) + $fib($n -2);
};

use (&$fib)& を省略すると動きません。関数を作成する時点で $fib 変数は定義されておらず、キャプチャするためにまだ定義されない関数をコピーすることもできないからです。リファレンスキャプチャではこの制約を超えられます。

mapと関数式

私は中学校の算数で挫折した人間なのでよく知りませんが、数学では関数ということばを写像(map)という用語で置き換えることができるようです。プログラミング言語の機能では、集合に対して関数を適用する操作にmapという名前が付けられることがあります。PHPではarray_map()という、関数に対して関数を適用する機能があります。

中学校の算数の授業で習った関数や方程式は $f(x) = x \times 2$ や $y = x \times 2$ のような形式で書くことができ、以下のような表にすることがありました。

x 0 1 2 3 4 5 6 7 8 9
y 0 2 4 6 8 10 12 14 16 18

この関数をPHPの関数式で表すと、以下のようにできます。

$f = function ($n) { return $n * 2; };

さらに、PHPでこのような表を作るために、さきほど紹介したarray_map()を利用できます。

$x2 = function ($n) { return $n * 2; };
print_r(array_map($x2, range(0, 9)));
// Array
// (
//     [0] => 0
//     [1] => 2
//     [2] => 4
//     [3] => 6
//     [4] => 8
//     [5] => 10
//     [6] => 12
//     [7] => 14
//     [8] => 16
//     [9] => 18
// )

PHP 7.4で追加されたアロー関数(short function)

function式のデメリットは、単に一つの式を返したいだけでも function (...) { return ...; } の形で書かねばならず、たいへん野暮ったいことです。PHPにとって関数式の先輩であるJavaScriptにはECMAScript2015でアロー関数が導入されました。

それに範をとってPHPにも2015年頃から幾度と短い関数式記法が提案されてきましたが3、2019年にようやくPHP: rfc:arrow_functions_v2という提案が受理され、PHP 7.4で正式に利用できるようになりました。

Arrow Functions 2.0仕様のアロー関数は以下のような記法です。

fn(parameter_list) => expr

JavaScriptのアロー関数とは異なり、引数リスト部は必ず fn() で括らねばなりません。また => の右辺に欠けるのは単一の式です。

$before = array_map(function ($n) { return $n * 2; }, range(0, 9));
$after  = array_map(fn($n) => $n * 2, range(0, 9));

引数や型宣言の仕様は既存のfunction関数式と同様です。

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

このアロー関数のおいしいところとしては use () で変数の明示的なキャプチャが不要になったことです。

$f = [];

// Before
foreach (range(0, 10) as $n) {
    $f[$n] = function () use ($n) {
        return $n;
    };
}

// After
foreach (range(0, 10) as $n) {
    $f[$n] = fn() => $n;
}

キャプチャが不要になったことは、現状の文法では fn と組み合せて useを書くことはできないことの裏返しでもあり、既存のfunction式では可能だったがアロー関数には置き換えられない制約があります。function式と同じく$thisも暗黙的にキャプチャされますが、static fn()のようにstaticを前置すると束縛しないようになります。

PHP 7.4のアロー関数の制限

PHP RFC: Arrow Functions 2.0の提案にはFuture Scopeとして将来の機能拡張可能性がまとめられてます。これらの項目はRFCの著者が必ずしもそのようにしたいという意向を表したものではありません。

逆に言うと、これらの機能はPHP 7.4時点では使えない制限事項です。

複数の文

現状では fn($n) => $n * 2 のような式を書くことはできますが、 fn() => { $m = 2; return $n * $m; } のように複数の文を書くことはできません。つまり、いままで function () { ... } 式で書かれてきたものを直接置き換えることはできません。

RFCでは次のような拡張可能性があることを示唆してます。

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

// 可能なら単にこうする
fn(params) {
    stmt1;
    stmt2;
    return expr;
}

複数の文が受け入れられるようになった代りに return が必須になったので、後退したように感じられます。

使用が自明ではない変数はキャプチャされない

RFCでは以下のようなコードで$xはキャプチャしないと言及されています。

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

また、以下のようなコードでも同様にfunctionからfnに置き換えたときに動作しなくなります。

$n = 10;
$f1 = function () use ($n) { return $n; };
$f2 = function () use ($n) { return eval('return $n;'); };
$f3 = fn() => $n;
$f4 = fn() => eval('return $n;');

var_dump($f1());
var_dump($f2());
var_dump($f3());
var_dump($f4()); // Notice: Undefined variable: n

変数キャプチャ方法の切り替えはできない

function 式では function () use (&$var) と書くことで外部の変数のリファレンスを捕捉できました。アロー関数では現状リファレンスのキャプチャをサポートしないということは、今回の記事で取り上げた方法での再帰やカウンター関数は実装できないということです。

RFCでは以下のようなパターンでの構文拡張可能性があると記述されてます。

$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)

↑ のパターンはリファレンスで捕捉したい use(&$a) のみを記述しますが、 $b は省略されているので、$bはキャプチャするのかしないのか混乱させかねない問題があります。明示的に書くか、多少の混乱の恐れがあっても省略を維持するかのトレードオフがあります。

関数/メソッド定義文のアロー記法

今回導入されたアロー関数は従来の無名関数式の代替となるであり、関数やメソッドの定義文は対象外です。

しかし、関数やメソッドの実装が単一の式(式文)のみからなる場合はアロー記法で定義できるようにしても良かろうというアイディアもRFCに記載されてます。

class Test {
    private $foo;
    private $bar;
 
    fn getFoo() => $this->foo;
    fn getBar() => $this->bar;
}

将来のPHPバージョン(おそらく8系)でもし複数文記法と同時に受理されてしまうと、ソースコード中のfunctionが全てfnに置換されたりするのか? うーむ…

再帰が(素直に)できない

これまでに説明した通り、常識的な手法でPHPの無名関数を再帰呼び出しするには変数リファレンスのキャプチャが必須だからです。その制約を乗り越える方法はいちおうあって、不動点コンビネータによる無名再帰です。Wikipediaを読んでぴんとこなければ、がんばって理解する必要は特にないです。

<?php

$fix = fn($f)=>(fn($x)=>$f(fn(...$as)=>($x($x)(...$as))))(fn($x)=>$f(fn(...$as)=>($x($x)(...$as))));
$fib = $fix(fn($fib) => fn($x) => ($x < 2) ? 1 : $fib($x - 1) + $fib($x -2));

var_dump($fib(6)); // 13

これは変なコーディング用のテクニックなので、実用的に真似する必要はまったくないです。だいたい再帰呼び出し以外のアルゴリズムで書いた方がパフォーマンスも出るし…

最後に

この記事の内容は @perpouh さんのスターリンソートwithPHPアロー関数でやろうと思ってたと書いてあって、「まじか、それってめっちゃ難易度が高いのでは……?」と感じたのがきっかけです。実際にやってみると、難易度超激難ってほどではないものの、やっぱり「ちょっとついでにアロー関数で書いてみっか」って気分でやれるほど気楽でもなかったです。結局やったんですけど。PHP縛りゲー大好きなので、次から何か適当な処理を書いてみたくなったら次もアロー関数縛りでやってみたいです。ありがとうございます。

おまけ: 式の中に文を書く方法

これまでさんざん「式と文は繋げられない」とか「文の中には式しか書けない箇所がある」とか言ってきたのですが、実際には従来のfunction式であれば複数の文が記述できるので、その制約を乗り越えることができます。

つまり、引数の中にforeachを書いたりifの条件式を書くところの中でクラスを定義したり、文字列リテラルの中にめっちゃ複雑な式を書いたり、なんでもやりたい放題です。楽しいですよ。

脚注

  1. PythonやSchemeのような言語はこの方法で関数内ローカルな関数の定義が許されます

  2. evalすれば可能ですが…

  3. PHP: rfc:short_closures, PHP: rfc:arrow_functions

166
145
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
166
145

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?