PHP式プログラミング入門
式、書いてますか? 私は書いています。この記事を書いているみなさまも、おそらく書いているのではないでしょうか。
本稿では私が大好きな式プログラミングというスタイルのコーディングテクニックについて紹介します。
初めに申し上げたいのは、この記事の内容をお読みいただくことでPHPやプログラミング言語についての知識をある程度まで理解することはできますが、この記事が目指すポイントはある種のパズルであり、非実用的なテクニックでもあります。
しかし、私は思うのです。実用的なテクニックとは日常と過剰の間のどこかにあるのではないかと。
雑に眺めるPHPの風景
みなさまは普段、PHPのコードの規則について意識して書いてますでしょうか。
それをアバウトに表現すると、以下のように表現できるでしょうか。
-
.php
ファイル内のテキストはそのままHTMLとして出力される - スクリプトは
<?php ... ?>
または<?= ... ?>
の中に書く -
$x = 1;
で変数代入 -
var_dump(100 * 1.1);
で結果表示 -
$dt = new DateTime();
で日時を表すオブジェクトを作成 -
echo $dt->format('Y-m-d');
で日付を画面に表示 -
if ($a === $b) { x(); } else { y(); }
で$a
と$b
が同じなら関数x()
を、違えば関数y()
を実行 -
foreach ([1, 2, 3] as $n) var_dump($n);
で1, 2, 3
を順に表示
……などなど。
いささか適当に書きすぎてしまったきらいはありますが、このように列挙していくことで雑ではありますがPHPというものを説明していくことは一応できます。この記事ではPHPの構造を言語化して理解の解像度を上げることを中間の目標とします。
Lispの世界
さて、いきなりですがPHPの話を進める前に別のプログラミング言語の話をしましょう。みなさまはいままでどんな言語を経験してきたでしょうか。現在においては非常に多種多様なプログラミング言語があります。
独学でRubyなどを書いてきたかもしれませんし、JavaやC言語の講義などを受けてきたかもしれません。機械学習のためにPythonを勉強してきたかもしれません。商業高校や高専の授業でVisual Basic、Scheme、あるいはProcessingのような言語に触れたかもしれません。またはPHPが始めてのプログラミング言語だという方も少くないかもしれません。
それまでやってきた言語とPHPは「同じようなもの」と感じたでしょうか。それとも「全然違う」と感じましたでしょうか。
この記事で主題とするのはPHPですが、ある言語の特徴は、ほかの言語と比べて初めて際立つものです。
言語が似ていると感じるか全然違うと感じるかは個々人の経験によって大きく異なるかと思いますが、ここで一度PHPと全然違うプログラミング言語、Lisp(リスプ)を紹介しましょう。
(defun fib (n)
(if (> n 1)
(+ (fib (- n 1))
(fib (- n 2)))
1))
(let ((n (number-sequence 0 12))
(r '()))
(while n
(setq r (cons (fib (car n))
r))
(setq n (cdr n)))
(nreverse r))
このコードはEmacs上で実際に動作します。
PHPの世界
括弧だらけの世界からおかえりなさい!
<?php
function fib($n) {
if ($n > 1) {
return fib($n - 1) + fib($n - 2);
} else {
return 1;
}
}
$n = range(0, 12);
$r = [];
while ($n) {
array_unshift($r, fib($n[0]));
array_shift($n);
}
$result = array_reverse($r);
echo json_encode($result);
先述のLispコードをほぼ対応するようにPHPに直訳してみました。Lispでは数列を表現するために連結リスト(単方向リスト)を、PHPではarray(動的配列)を利用しているというデータ構造の差異があります。その差異を維持しているため、一般的なPHPプログラムとしては非効率な実装になっています。
もうすこしPHP風に書いてみましょう。
<?php
$r = [];
foreach (range(0, 12) as $n) {
$r[] = fib($n);
}
$result = $r;
echo json_encode($result);
データ構造について論じることは本稿の目的ではありませんのでこの件はさておき、次に進みましょう。
PHPとLispの最大の違いは何でしょうか。ここからはプログラミング言語の性質についてまとめます。
構造化プログラミング
これまでに挙げたLispとPHPのコードに表われた言語機能を比較してみましょう。
PHP | Lisp | |
---|---|---|
関数定義 | function($arg) { ... return $v; } |
(defun (arg) ... v) |
条件分岐 | if ($cond) { ... } |
(if cond ... ...) |
関数呼び出し | f($arg) |
(f arg) |
不等号演算子 | $a > $b |
(> a b) |
ローカル変数宣言 | なし | (let ((a 1) ...) |
変数代入 | $a = 1 |
(setq a 1) |
条件付き反復 | while($cond){ ... } |
(while cond ...) |
表にするとPHPとLispはこんなにそっくりだったのですね。 「PHPはLispの一種!」 などと適当な世迷言をいうのは簡単なのですが、実際には両者の間には大きな隔りがあります。
そもそも、多くの実用的なプログラミング言語はこれらの機能を備えています。
ウィキペディア日本語の『構造化プログラミング』の項ではハーラン・ミルズが提唱した経験則に基いて形式化したプログラムの構造を以下のようにまとめています。
- 順次(sequence)
部分プログラムを順々に実行する。- 選択(selection)
条件式が導出した状態に従い、次に実行する部分プログラムを選択して分岐する。- 反復(repetition)
条件式が導出した特定の状態の間、部分プログラムを繰り返し実行する。
(フリー百科事典 ウィキペディア『構造化プログラミング』 2020年12月24日版 https://ja.wikipedia.org/w/index.php?title=構造化プログラミング&oldid=81009524 より引用)
部分プログラムとは1つ以上のコードをまとめたものであり、文(statement)、ブロック、手続き(procedure)などのことです。手続きとはPHPでは聴き慣れない用語ですが、要は関数やメソッドなどのことで、処理を呼び出せる形式にまとめたものです。ただし言語によってその性質は異なり、サブルーチンなどと呼ばれることもあります。
一般的に、この三種類の構造と手続きおよび手続き呼び出しをベースにしてプログラムを記述する手法を構造化プログラミングと呼びます。
問題は世界にある数多のプログラミング言語がこれらの機能をどうやって提供するかです。
順次はコードを順番に実行する機能です。PHPでは関数呼び出しなどを ;
(セミコロン)で区切って連ねることで順に実行することができます。Lispなどの言語ではマクロと呼ばれる機能を用いて実行順を制御することもできます。
選択は一般的にはif
(then
) else
のようなキーワードを伴った構文として提供されることが多くあります。
反復はプログラムを繰り返し実行するための構造であり、よく知られたものはwhile
です。そのほかにもfor
あるいはforeach
というキーワードが用いられることがありますが、その機能は言語によって大きく異なります。
これらのプログラムの構造は汎用プログラミング言語と呼ばれる一般によく知られた言語の多くで実現されています。「プログラミングは一度マスターすれば他の言語も簡単に覚えられる」などと言及されることがあるのも(その是非はともかくとして)、その性質によるものです。
その一方である程度のプログラミング歴がある方でしたら、経験のない言語に新しく触れる際に、チュートリアルなどを一度学んだだけではまだその言語の熟練者と同等のパフォーマンスは発揮できないということは身に覚えはないでしょうか。
プログラミング言語には設計者の思想によって、さまざまな意図が込められています。プログラミングを学ぶということには言語の構文や機能の特性を学ぶこと、言語の組込み機能やデータ型、標準ライブラリやサードパーティー製ライブラリの開発コミュニティの動向を学ぶことなどが含まれています。
プログラミングの手法にはオブジェクト指向プログラミング(object-oriented programming, OOP)や函数プログラミング(functional programming)などがあり、プログラミング言語はそのような手法を支援するための機能が用意されていることが多くあります。どのような手法での記述を主眼に置くかによってプログラミング言語を分類することができ、これをパラダイムと呼びます。パラダイムは決して排他的なものではなく、複数のパラダイムを組み合わせたスタイルをサポートする言語をマルチパラダイム言語と呼びます。また、ある言語で有用であると知られた言語機能が、別のプログラミング言語の新機能やライブラリとして部分的に取り入れられるという流れもあります。
PHPの言語仕様の発展も常にそのようなトレンドの影響下にあります。
式と文
前置きがとても長くなってしまいました。記事冒頭でLispについて学んだのが遠い昔のようです。改めてPHPとLispの違いとは何でしょうか。
それぞれの言語の構文の決定的な違いは以下の二点に集約できるでしょうか。
- PHPには行末に
;
がある - Lispには
return
がない
もう一度Lispの関数定義を掲載します。
(defun fib (n)
(if (> n 1)
(+ (fib (- n 1))
(fib (- n 2)))
1))
こちらはPHPに書き写したコードです。
function fib($n) {
if ($n > 1) {
return fib($n - 1) + fib($n - 2);
} else {
return 1;
}
}
たしかにLispコードにはreturn
がなく、PHPにはreturn
が2つあります。このLispコードを正しく理解するには、Lispのif
は値を返すということを把握しなければなりません。またLispの関数定義では、最後に実行されたコードが返した値を返します。この最後に実行された値のことを評価値といいます。
PHPのif
には、もちろんそんな性質はありません。 $n = if($cond) return 1;else return 2;
なんてコードは今のところは許されません。
つまり、PHP実装をLispコードに近付けるには以下のようになるでしょう。
function fib($n) {
return ($n > 1)
? fib($n - 1) + fib($n - 2)
: 1
;
}
この?
と:
は条件演算子といいます。 $f = ($n == 9) ? 1 : 2
と書いたとき、$n
が9
ならば$f
に1
が、それ以外なら2
が代入されます。「条件式」「条件が合ったときに評価する項」「条件が合わなかったときに評価する項」という3種類の項(引数)を渡すことから三項演算子と呼ばれることもあります。
評価(evaluation)という言葉に馴染みがないかもしれませんが、これは先程説明した通り「コード/関数を実行」というのとほとんど同じ意味です。これはPHPマニュアルでも使われている用語ですので、ぜひ記憶に留めておいてください。
演算子(operator)と式(expression)という用語が出てきました。これらの用語は中学校か高校の数学で聞いたことがあるかもしれません。(用語の意味は大ざっぱに共通していますが、厳密に同じではありません。)
さて、PHPの構文は式と文(statement)という要素から構成されています。式は1つ以上の項(term)と0個以上の演算子からなります。この項という用語にはいくつかの意味があり、なおかつ厳密には定義されていません。
項の最初の意味は1
や"str"
のようなリテラル、$var
のような変数、PHP_EOL
のような定数などです。次の意味は1 * 2
や"a" . "b"
、f(1, 2)
のような、最初の意味での項が演算子などでまとまったものです。
また、先ほどの三項演算子での説明でもあったように、引数(argument)と同じような意味で使われることもあります。これらの用例はどれも、項を評価(実行)するとなんらかの値を返す(評価値がある)という点で共通しています。式という用語は二つめの意味での項と共通しています。
式は、ある式の一部分になることができます。
たとえば$v = (1 + 2) * $w
というコードに対して、 1
, 2
, (1 + 2)
, $w
, (1 + 2) * $w
そして$v = (1 + 2) * $w
という、それぞれの部分を切り取っても式です。そして関数呼び出しなども式であり、var_dump($v = (1 + 2) * $w)
とさらに外側に括っていっても、相変らず式です。
このように式とは、値あるいは値を返すような式(演算子や関数呼び出しなど)で繋げたものをいいます。式という用語を説明するために式ということばが出てきてしまいました。このような構造を再帰(recursion)といいます。
さて、PHPでプログラムを書いていくと、式を延々と続けていくばかりでないことに気付きます。そうです。式は適度な箇所で;
(セミコロン)で区切るのです。
変数代入は式ですが、一般的には$v = pow(1,2);
のようにセミコロンで終端させます。同様に var_dump(1);
のように関数呼び出しをしてすぐに終端させることがあります。このように式によって構成される文を式文(expression statement)といいます。
ここで文(statement)という用語が出てきました。PHPでは以下のものが文です。
- 式文
- 選択文
if
else
elseif
switch
- 反復文
while
for
foreach
do
- try文
try
catch
finally
- declare文
declare(strict_types=1)
- 関数定義文
function
- クラス宣言文
class
trait
interface
- 定数宣言文
const
- ラベル文, goto文
- namespace宣言文 use宣言文
- echo文, unset文
- global変数宣言文
- 関数静的変数宣言文
static
これらの文や式は、一般的に特定の英単語のようなもの(キーワード)からはじまる構文から開始されています。構文のキーワードは関数名や定数名、クラス名として使うことはできず、予約語とも呼びます。予約語はPHP 7.0以降はメソッド名には利用できるようになりました。
また、あるキーワードが別の構文にも使われることがあります。たとえばuse
はnamespaceのuse
宣言とトレイト追加の別の意味があります。
それとは別に、似た機能の式と文の違いとして提供されているものがあり、キーワードが同じものもあります。
コードを読んでいて、実装の途中にいきなりfunction () { ... }
のように名前のない関数が出てきて面喰らった経験はありませんでしょうか。PHPではローカル関数定義の機能がない代りに、関数式を値として受け渡しができます。
PHPのクラスやグローバル関数は、それ自体を変数に入れて扱うことはできません。しかしfunction式で作成される関数はClosure
クラスのオブジェクトであり、変数に代入したり引数として渡したりすることもできます。
また、PHP 7.4ではアロー関数式という構文が追加されました。これは戻り値となる式をひとつだけとり、function式よりも短く書ける無名関数です。コード例は後で紹介します。
そして式プログラミングへ
前提の説明でここまで紙幅を使ってしまいました。話を急ぎましょう。
Lispはif
も式であり、関数にreturn
はなく関数の最後に書いた式が返り値となるなど、あらゆるものが式である式指向の言語です。PHPはというと、式であるものと文であるものが混在しています。もっともこれはPHPだけの特徴ではなく、C言語やJavaScript、Pythonなど多くの言語に共通します。ただしRubyは式と文について、Lispに近い性質があります。
PHPにおいて式プログラミングを実践するということは、言い変えると「式文以外の文を可能な限り排除する」というスタイルです。それでいて、順次・選択・反復の三大構造を実現する方法を考えなくてはいけないのです。
式プロのレギュレーション
PHPにおいて式プログラミングを実践するには<?= ... ?>
形式を多用します。これは<?php echo ...; ?>
と同等です。
この形式に落としこむことで、ファイル内に式文がひとつだけの状態にできます。
順次
PHPにおいては式と文を上から下に、左から右に順番に実行するのが順次実行なのでした。
以下のコードは日時を表示します。
<?php
$datetime = new DateTime();
$fmt = 'Y-m-d H:i:s';
$ymd = $datetime->format($fmt);
echo $ymd, PHP_EOL;
あからさまに冗長なコードは以下のように短縮できます。
<?= (new DateTime)->format('Y-m-d H:i:s') . PHP_EOL ?>
変数をやめただけですが、文字数も行数も、たしかに減って減っています。先述のレギュレーション通り<?= ... ?>
で括っていますが、これがプログラムの全てです。<?php ... ?>
を書くことはありません。
選択
選択とは、代表的なものはこれまで述べた通りif
や三項演算子が代表的です。それ以外にも短絡評価演算子を使う方法、配列を使う方法、match
式を使う方法などがあります。
短絡評価(short-circuit evaluation) とは、式全体を評価しなくても結果を確定できるとき、必要のない箇所を実行しない性質のことです。たとえば$f(1) || $f(2) || $f(3)
という式を考えてみましょう。 $f
の関数はなんでもよいのですが、$f = fn($v) => $v % 2 == 0;
としておきましょうか。つまり偶数ならtrue
を、奇数ならfalse
を返します。さきほどの式は「$f(1)
、$f(2)
、$f(3)
のどれかがtrue
を返すか」というふうに読めますが、この結果を求めるには$f(2)
を呼び出してtrue
が返された時点で結果が確定でき、その次の$f(3)
を呼び出す必要がありません。必要のない処理を省略するのが短絡評価です。
PHPの短絡評価演算子には&&
||
and
or
xor
?:
??
があります。かつてデータベースに接続する処理において$conn = mysql_connect() or die(1);
のようなイディオムが多用されていたことがありました。これは接続に失敗したらその場でプログラムを終了することを意味しています。
これは別の書きかたをすると
if (!($conn = mysql_connect())) {
die(1);
}
と同じ意味になります。
次に配列を使った条件分岐というものを考えてみましょう。
echo [
[$n, 'Buzz'],
['Fizz','FizzBuzz']
][$n%3==0][$n%5==0];
これは配列を作成してすぐに結果を取り出す式ですが、変数$n
が3の倍数か5の倍数か、その公倍数を求めるという条件分岐が成立しています。比較演算子==
の結果はbool
型であり、配列のキーのbool
は1
, 0
に変換されるので要素が取り出せるのです。
また、関数を使った値の選択ということもできます。たとえば、max()
関数は引数に渡した値の一番大きいものが、min()
関数は一番小さいものが返されるので、三項演算子よりもさらにコード量が圧縮できます。
$n = ($id > 0) ? $id : 0;
$n = max($id, 0);
一方で min()
, max()
は単なる関数なので短絡評価されないことには注意が必要です。
反復
反復はプログラムを繰り返し実行することでした。PHPでは一般的に繰り返しにはforeach
やwhile
を使います。
単純な反復は関数の再帰呼び出しで実現できます。以下の1から10までの合計値を求める関数を考えてみましょう。
<?= call_user_func(
$f = function (int $n) use (&$f) {
return ($n <= 0) ? 0
: $n + $f($n - 1);
}, 10) ?>
これは初期値10から1減らして、0以下になったら再帰呼び出しを停止します。
そうすることでwhile
を避けてループすることができました。しかし、この方法には欠点があります。終了条件($n <= 0)
や再帰呼び出しの引数$n - 1
をうっかり間違えると無限ループに陥ってしまいます。
できればそのような細かい条件判断は減らして難しくなく書けるようにしていきたいところです。そこでarray_reduce()
を使ってみましょう。
<?= array_reduce(range(1, 10), fn($sum, $n) => $sum + $n, 0) ?>
array_reduce()
は配列の中身を最終的な単一の値にまとめる関数です。極めればかなり使いでのある機能なので詳細に語りたいのはやまやまなのですが、一般的なPHPではforeach
を使えば十分なこと、紙幅の制約から紹介のみに留めます。
さて、今回のケースでは足し算なのでarray_reduce()
を使わずともarray_sum()
だけでも解決可能です。しかし足し算ではなくすべての数字を掛け合わせた値を求めたいときは、上記の式の+
を*
に変えるだけで対応可能で応用性のある機能です。
ここで fn() => ...
という式が出てきました。これはPHP 7.4で導入されたアロー関数で、無名関数式の function () { return ...; }
を短く書けます。無名関数(Closure
)については別稿で詳しく解説したので、興味があればお読みください。
ここで説明したいのは、一度array
というデータ構造にすることは式プログラミングと相性がよいということです。
先述したFizzBuzz問題のコードを思い返してみましょう。よくある解法では1から100まで反復実行して、その都度echoなどで出力します。これを別のアイディアで解いてみましょう。
まず[1, 2, 3, ..., 100]
という配列を用意します。それを[1, 2, Fizz, ..., Buzz]
という別の配列に変換するのではどうでしょうか。
この変換はarray_reduce()
でも可能なのですが、array_map()
という専用の関数が用意されています。
例として1から100までの数字をすべて3倍にしてみましょう。
<?php
$result = array_map(
fn($n) => $n * 3,
range(1, 100)
);
このように関数呼び出し結果の値を得ること、あるいは配列のような値の集まり(集合)のそれぞれの要素に対して関数呼び出し結果が要素となるような配列を得る操作を適用(apply)といいます。
このコードで求められる値の変換は小学校の算数の比例や中学校の数学の1次方程式に似ています。
あとはこれを出力すれば問題解決ですが、PHPは配列をそのまま出力できません。配列を文字列に変換する必要があります。この変換もやはりarray_reduce()
で可能ですが、それよりもimplode()
関数(別名:join()
)を使うのが簡単です。
<?= implode("\n", [1, 2, 3]) ?>
このコードは配列の内容を改行区切りで出力します。ここまでのアイディアを組み合わせれば1〜100のFizzBuzzやフィボナッチ数列を出力するコードも書けます。
あとがき
前半はプログラミング言語比較からプログラミング言語の構造と用語の説明、後半では構造化プログラミングの各処理を文ではなく式で実現する方法を解説しました。
世の中にはさまざまな「縛りゲー」があり、たとえばセミコロンレスJava、Pythonワンライナーなどです。PHP式プログラミングは両者の特徴を併せ持っていますが、言語仕様により実際にやってみると難易度は他の二つの言語よりかなり低いです。また、本稿で示した通り構造化プログラミングが可能だということは一般的なPHPプログラムはすべて式に置き換えられます。
ただ可能だということ、やるべきだということは、まったく別の事柄です。一般的には変数は式や値に名前を付けて読みやすくするために用いられます。Lispは式プログラミングに特化した言語なので違和感なくプログラミングできますが、PHPはそうではないために無理が生じ、デバッグの難易度も非常に上がってしまいます。
本稿で述べたテクニックのすべてを一度に使うと実用的に読みにくいコードになりますが、それぞれをピンポイントで使うぶんには簡潔ですっきりしたものになります。PHPの言語仕様や関数を知り、過剰にならないようバランスを取りながら使うことでコードの価値を高めていきましょう。
本稿では書き足りなかったテクニックは https://scrapbox.io/php/式プロ に加筆します。PHPを楽んでください!
Discussion