読者です 読者をやめる 読者になる 読者になる

PHPとかいろいろ演算代入系の演算子のハナシ

PHPの関数定義はこんな変態的な書き方ができる - 頭ん中

に、続いて。アンリーダブルコードで勉強しようというのがあった、そのとある勉強会の発表ネタです。

これは、PHPのMarkdownパーサ の実装を可能な限りそのまま綺麗に変換してJavaScriptに移植しようという js-markdown-extra をやっていたとき、大ハマりして修正に苦労したバグの話から来てます。

演算代入。+= とか *= とかのやつ。関数型の人以外はきっと常用してますね。じゃあ問題。

<?php
$tokens = array("a", "b", "c");
$tokens[0] .= array_shift($tokens);
print_r($tokens);

こうするとどんな結果が出力されるでしょうか。PHPです。

自身の先頭から要素を取り出して、それを先頭要素に文字列追加する。array_shift() では "a" が先頭から取り出されて $tokens == array("b", "c") になります。取り出された値は配列の先頭要素に「文字列として追加」されて配列の先頭要素に代入されます。結果を見ましょう。

$ php test.php
Array
(
    [0] => ba
    [1] => c
)

...って思いますよね。ここまで、いいですか、よくないですか。

ところがPHPは... という話だと思ったら大間違い。もしPHP以外の言語の人なら、間違っているのはあなたのプログラミング能力ですよと。PHPerはこれでOKです。

JavaScriptで書きました。

var tokens = ["a", "b", "c"];
tokens[0] += tokens.shift();
console.log(tokens);

なんとこれ、答えはこう出ます。

$ node test.js
[ 'aa', 'c' ]

え、え、まあ JavaScript は変な言語だからさあ...

そこにPython委員長がやってきた。

tokens = ["a", "b", "c"]
tokens[0] += tokens.pop(0)
print tokens

$ python test.js
['aa', 'c']

やばい!! PHPちゃんが多数決で負けちゃう。助けてRubyちゃん。(嫌なこと言われることもあるけど、最近嫌いじゃないのよね、あの子。)

tokens = ["a", "b", "c"]
tokens[0] += tokens.shift
p tokens

$ ruby test.rb
["aa", "c"]

あああああーーーっ、Ruby子ちゃんまで。PHPちゃんボッチかわいそう。

検証

すんません、ちゃんとやります、はい。ようするに、a += ba = a + b と等価ってのがわかればいいんですよ、きっと。JavaScriptで検証、やってみましょう。

function equal_and_plus()
{
    var tokens = ["a", "b", "c"];
    tokens[0] = tokens[0] + tokens.shift();
    console.log(tokens);
}

function equal_plus_operator()
{
    var tokens = ["a", "b", "c"];
    tokens[0] += tokens.shift();
    console.log(tokens);
}

equal_and_plus();
equal_plus_operator();

[ 'aa', 'c' ]
[ 'aa', 'c' ]

合格ですね。PythonRubyもこれと同じですね。じゃあPHPはいったい...

<?php
function equal_and_plus()
{
    $tokens = array("a", "b", "c");
    $tokens[0] = $tokens[0] . array_shift($tokens);
    ptint_r($tokens);
}

function equal_plus_operator()
{
    $tokens = array("a", "b", "c");
    $tokens[0] .= array_shift($tokens);
    ptint_r($tokens);
}

equal_and_plus();
equal_plus_operator();

Array
(
    [0] => aa
    [1] => c
)
Array
(
    [0] => ba
    [1] => c
)

違う演算じゃんかこれ

そのとおり、PHPの場合は「加算と代入」は「加算代入」と等価ではなかったのです。ちょっと最初に動かす前に想像した結果に戻ってみてください。「取り出して」→(右辺の変数が変化して)→「代入」って考えましたよね。PHPはそのとおり動きました。

でも他の言語では実際には、「加算代入は加算と代入である」という等価関係を死守します。その結果、最初に評価されるのは tokens[0] = tokens[0] + shift(tokens) 左辺の第1項 tokens[0] つまり "a" です。それに、shift で先頭要素を取り出して詰めた戻り値の "a" が足されます。そして、["b", "c"] という2要素の配列となったものの最初の要素 "b""aa" が代入されます。

たしかにPHPは、言語としての美しさの点では劣りました。が、そもそも「加算と代入」は「加算代入」と等価であるという言語仕様は、すべての処理系で保証されるのでしょうか。「加算代入」という等価変換できない別の演算があるのだと考えればどうでしょう? そう考えるとPHPは、人間らしい自然な思考に合わせて、「加算と代入」が「加算代入」と等価ではない処理系を選んでいるとも言えます。

まあ、こういうことですね。

<?php
function eval_right_then_set_to_left()
{
    $tokens = array("a", "b", "c");

    // 右辺の値
    $right = array_shift($tokens);

    // 左辺に入る値を算出
    $left = $tokens[0] . $right;

    // 代入のところ
    $tokens[0] = $left;

    ptint_r($tokens);
}

そもそも、二者が等価でなければならない理由ってあるんでしょうか。C言語と違うから? 違う言語は違う処理系を持っていて当然ですね。Cはコンパイラにとってコンパクトな仕様だったから等価にしただけかも。ルールが少ないほうが文法の学習コストが低い? 言語の文法なんて他の学習の総コストに比べたら低いですよね。マニュアルに「同じじゃないよ」と書いてあればそう学べばいいんですよ。

PHP: 代入演算子 - Manual

$a = 3; $a += 5; // $a を 8 にセットします。$a = $a + 5; と同じです。

ギャフン

こ...こんなことが宝石系の言語の人に知れたら、またはてブが炎上するに違いない。こりゃ門外不出だぜ。なんかアレみたいな話だな。

と思ったんですが、試しに普段あまり使わないPerlでもやってみました。

# 加算して代入
@tokens = ("a", "b", "c");
$tokens[0] = $tokens[0] . shift(@tokens);
print join(',', @tokens) . "\n";

# 加算代入演算子
@tokens = ("a", "b", "c");
$tokens[0] .= shift(@tokens);
print join(',', @tokens) . "\n";

とりゃー

aa,c
b,c

なんと、誰も期待しなかった結果を返す言語は実はPerlでした。

これ、結果の解釈いろいろ深いですね。なんかこんな感じ? せっかく入れた "aa" を最後に消しちゃう。んーよくわかんない。

@tokens = ("a", "b", "c");
$tokens[0] .= $tokens[0];  # shiftからの戻り値のみ
shift(@tokens);  # shiftの実行そのもの
print join(',', @tokens) . "\n";

(よしこれで白い宝石の方からのマサカリは飛んで来ないぞ...と思うんだけど、もし、Perlではこう書くのが普通だよというのがあったら教えてください)

(ブクマコメントで @tokens[0] がスライスになってると教えてもらいました。やってもうた。$ に修正して結果が同になることを確認しました。)

というわけで、これって怖い人に「PHPなんてものを使うことがそもそも...」とか言われる系の話じゃなくて、

見た目がまったく同じなのに言語処理系によって非互換性なところはあるので、どの言語を使うにしても、直感で人によって解釈が分かれるようなコードになるおそれがある場合は、他人が読んでもわかるよう、自分の頭の中にある理解の順を正しく表現したコードにしましょう、

というのが大事ですねという話でした。

これって、純粋関数型で再代入を禁止するべきとかそういう極端な話じゃなくて、プログラマーの素朴な人間力ってだけだと思います。処理系が違えば言語仕様は違って当たり前なのに、自分の好きなアレと違うからダメな言語と言ってしまうのは良くないよね、とか、字面が読みやすいかどうかではなく、こういうのを分けて書かないとアンリーダブルで大変なことになるんだという学びとか。

あ、フィクションじゃないですよ。ホントにあったんですよこれ。

https://github.com/tanakahisateru/js-markdown-extra/commit/1a0b98b32cf1a94b2e87efd960190f5c47243f46#L0L1161

そうそう PHP といえば

こちらもよろしくお願いします!

なぜ『PHPエンジニア養成読本』はAmazon部門ランキングでトップを取るのか

PHPエンジニア養成読本 〔現場で役立つイマドキ開発ノウハウ満載! 〕 (Software Design plus)

PHPエンジニア養成読本 〔現場で役立つイマドキ開発ノウハウ満載! 〕 (Software Design plus)

PHPエンジニア養成読本