PHP 5.3の新機能と変更点

第2回クロージャとgoto文

クロージャ

クロージャとは関数の一種で、多くのスクリプト系言語がサポートしている機能です。Perl、Ruby、Pythonなど、モダンなスクリプト系言語すべてがサポートしています。JavaScriptもクロージャをサポートしいます。JavaScriptプログラミングではクロージャがよく利用されるので使い方をご存知の方も多いと思います。

クロージャは関数がアクセス可能な変数が、環境によって決まります。この特徴を利用してオブジェクト指向設計よりも分かりやすい設計が行えたり、呼び出された関数の内部から呼び出すコールバック関数として様々な動作を行わせることが出来ます。

クロージャの使い方

クロージャはラムダ関数、無名関数と呼ばれることもあります。無名関数の名前の通りクロージャは名前が無い関数を定義して利用します。

クロージャに慣れていない方は、名前が無い関数をどうやって使うのか?どうして便利なのか?と疑問に思うかも知れませんが、例をいくつか見れば、その便利さが分かります。

クロージャの構文は次のように、関数定義の名前定義が無いものと言っても構いません。最後に⁠;⁠(セミコロン)があることに注意してください。

無名関数の構文
function( [ 引数 ] ... ) { };

PHP以外の言語では、グローバルスコープの変数に関数内部からもいつでもアクセスできるようになっています。PHPの場合は$GLOBALS変数を使用するかglobal宣言してから利用しなければなりません。無名関数の中からも同じような制限があります。

無名関数の中から無名関数が実行されるスコープ内の変数を使用する場合には、⁠use⁠文で使用を宣言しなければなりません。

use文を利用する場合の無名関数の構文
funciton( [ 引数 ] ... ) use ( [ 変数 ] ... ) { };

多少面倒に思うかも知れませんが、明示的に指定した変数以外はアクセスできないので、誤って意図しない変数を書き換えてしまったり、利用してしまうことを防ぐことができます。

無名関数は関数名が無いので、変数に格納するか、関数の引数として直接渡します。

:無名関数を変数から利用する
<?php
$writeline = function($msg) { echo $msg.PHP_EOL; };

$writeline('ABC');
$writeline('XYZ');
?>
出力
ABC
XYZ

これだけでは何が便利なのかさっぱり分からないですが、使い方だけは分かったと思います。

function($msg) { echo $msg.PHP_EOL; };

の部分が無名関数の定義です。無名関数は直接パラメータとしてほかの関数に渡すことが出来ます。

:無名関数を利用してarray_walkで配列の中身を出力
<?php
$fruits = array("lemon"=>156, "orange"=>210, "banana"=>180, "apple"=>343);
array_walk($fruits,function($price,$name) { echo $name.':'.$price.PHP_EOL; });
?>
出力
lemon:156
orange:210
banana:180
apple:343

無名関数が無いPHP 5.2場合、パラメータとして渡している

function($price,$name) { echo $name.':'.$price.PHP_EOL;}

を別の関数として定義した上で、変数関数としてarray_walk関数に渡さなければなりません。

:無名関数を利用しない方法
<?php
$fruits = array("lemon"=>156, "orange"=>210, "banana"=>180, "apple"=>343);
function callback($price,$name) { echo $name.':'.$price.PHP_EOL; }
array_walk($fruits,'callback');
?>

あまり、複雑でないのでわかりづらいかも知れませんが、無名関数を利用すると簡潔に記述できることが分かります。

無名関数を使うと、クラスを使ったオブジェクト指向設計を行わなくても、一つの関数に複数の動作を行わせることも簡単です。

:処理を無名関数として渡す
<?php
$square = function($x) { return ($x * $x); };
$increment = function($x) { return ++$x; };

function foo($param, $op) {
    return $op($param);
}

echo foo(3, $square) . PHP_EOL;
echo foo(4, $increment) . PHP_EOL;
?>

変数関数に書き換えると次のようになります。

:変数関数の場合
<?php
function square($x) { return ($x * $x); }
function increment($x) { return ++$x; };

function foo($param, $op) {
    return $op($param);
}

echo foo(3, 'square') . PHP_EOL;
echo foo(4, 'increment') . PHP_EOL;
?>

どちらも全く同じことができますが、大きな違いがあります。変数関数の場合、名前付き関数として定義しなければならないため、名前空間を使用していしまいます。処理する変数が数値でなく配列の場合は要素すべてを2乗したりインクリメントしたりする、という関数を新たに追加する場合に⁠square⁠という関数名は既に使われているので使えません。

‘square_array⁠などのように別の名前を付ければよいのですが、次々に新しい名前を付けなければならないかも知れません。

また、uasort関数の比較を行うコールバック関数のように、その場限りのコールバック関数に名前を付けるのは面倒です。

:配列のソート
<?php
$arr = array(
             array(12,33,1),
             array(21,2,105),
             array(3,99,3),
            );

$elem = 1;
uasort($arr, function($x, $y) use ($elem)
       {
            if ($x[$elem] == $y[$elem]) return 0;
            return (($x[$elem] < $y[$elem]) ?-1 : 1);
       }
);
print_r($arr);
?>
出力
Array
(
    [1] => Array
        (
            [0] => 21
            [1] => 2
            [2] => 105
        )

    [0] => Array
        (
            [0] => 12
            [1] => 33
            [2] => 1
        )

    [2] => Array
        (
            [0] => 3
            [1] => 99
            [2] => 3
        )

)

この例では多次元配列ですが、要素がオブジェクトである場合もソートしたいかも知れません。名前付き関数を定義した上で利用しなければならない仕様は、無名関数に慣れているプログラマに取っては苦痛でしかありません。

次はよりクロージャの便利さが分かる、関数が定義されたコンテクストの変数(レキシカル変数)を利用したクロージャの利用例です。便利さ分かる例として、配列に格納された商品に消費税を付けて合計するプログラムを作ってみます。

:レキシカル変数の利用
<?php
function sum_includes_tax($items, $tax) {
  $total = 0;
  array_walk($items, 
    function($price,$name) use (&$total, $tax)
    {
      echo $name.':'.$price.'('.(int)($price*$tax).')'.PHP_EOL;
      $total += ($price+(int)($price*$tax));
    }
  );
  return $total;
}

$items = array("lemon"=>100, "orange"=>200, "banana"=>300, "apple"=>400);
$total = sum_includes_tax($items, 0.05);
echo 'TOTAL:'.$total.PHP_EOL;
?>
出力
lemon:100(5)
orange:200(10)
banana:300(15)
apple:400(20)
TOTAL:1050

PHPはクロージャ(レキシカル変数)をサポートしていなかったので、array_walk関数の第3引数に任意のデータを渡せるようになっていました。PHPがクロージャをサポートしていれば必要なかった引数です。

:user_dataパラメータを使った場合
<?php
function print_elem($price, $name, &$user_data) {
  echo $name.':'.$price.'('.(int)($price*$user_data['tax']).')'.PHP_EOL;
  $user_data['total'] += ($price+(int)($price*$user_data['tax']));
}

function sum_includes_tax($items, $tax) {
  $user_data['total'] = 0;
  $user_data['tax'] = $tax;
  array_walk($items,'print_elem', $user_data);
  return $user_data['total'];
}

$items = array("lemon"=>100, "orange"=>200, "banana"=>300, "apple"=>400);
$total = sum_includes_tax($items, 0.05, $user_data);
echo 'TOTAL:'.$total.PHP_EOL;
?>

実はこのコードではクロージャを利用した例のように動作しません。コールバック関数のuser_data引数が参照として渡されていないからです。この結果、合計が計算されていません。同様の結果を得るにはグローバル変数を利用しなければなりません。

出力
lemon:100(5)
orange:200(10)
banana:300(15)
apple:400(20)
TOTAL:0

クロージャ(レキシカル変数)を利用した例とクロージャを利用しない例を見比べれば、クロージャを利用したほうが簡潔でコードも分かりやすく、不必要な関数で名前空間も利用しないことが分かります。

クロージャ(無名関数)を使用したことがない方は、慣れるまでは名前が無いことに違和感があるかも知れません。しかし、クロージャは簡潔かつ柔軟なコードを記述するために便利なので覚えておくとよいと思います。

制限付きgoto文

PHPにはプログラムの実行を自由な場所に移動するgoto文がサポートされていませんでした。goto文の乱用は簡単に理解不能なプログラムを作ってしまうからです。

しかし、goto文を利用するとエラー処理や特定の条件を満たした場合にループを抜ける処理が簡潔に記述できます。goto文があるほうが分かりやすく記述できるのです。

Visual Baiscでプログラミングをした経験がある方であれば、Visual Basicではエラーが発生した場合にgoto文でエラー処理のコードまでジャンプするようなコードをよく見かけていると思います。PHP 5.3からは同じようなエラー処理が可能になります。

goto文の使い方

構文
goto ラベル;

goto文の使い方は説明するまでもないと思います。以下にサンプルコードを書いておきます。

:単純なgoto文
<?php
goto A;
echo 'skipped';
A:
?>
:goto文の乱用も可能
<?php
goto A;

B:
die('stupid code');

A:
goto B;
?>
:ループやswitch文にはジャンプできない
<?php
goto A;

for ($i=0; $i<10; $i++) {
        echo $i;
A:
}
?>
出力
Fatal error: 'goto' into loop or switch statement is disallowed in /home/yohgaki/ext/src/php/php-5.3.0/e2.php on line 2
:ループから抜け出るのはもちろん可能
<?php
for ($i=0; $i<10; $i++) {
        echo $i;
        if ($i == 5) goto A;
}

A:
?>
出力
012345

goto文によりコードが大幅に読みやすくなる場合以外は極力利用しないようにするほうが良いでしょう。上の例のような単純にループから抜け出る場合は、break文が使用できます。

:break文
<?php
for ($i=0; $i<10; $i++) {
    echo $i;
    if ($i == 5) break;
}
?>

ループ処理の例外処理はほとんどの場合、break文、continue文で分かりやすく処理できます。goto文を利用するよりも、まずbreak文、continue文の利用を考えるようにしたほうが良いでしょう。


次回はPHP 5.3で追加された機能と、仕様変更について取り上げます。

参考文献
PHP マニュアル
PHP 5.2 to 5.3マイグレーションガイド
PHP 5.3ソース

おすすめ記事

記事・ニュース一覧