しばしば三項演算子やdo-while構文が禁止されているコーディング規約に遭遇する。それは今すぐ撤廃すべきという論理武装を考えてみよう。事例はPHPだが、このことはすべての言語にいえる。
例
あるサービスから何らかのデータを取り出し、それをクライアント側で使うという手続きを考える。
<?php
$client->useSomeData($service->getSomeData());
ここで、サービス側のAPIとクライアント側のAPIに仕様ギャップがあったとしよう。$service
は古く、うまく行かなかったとき例外ではなく null
的なもの(PHPではもしかするとfalse
かもしれない)を返す習慣で作られていた。いっぽう $client
は null
を入力される想定がない。何もしないで欲しいときは空の array
を入力する仕様だった。
以下の実装例を見てほしい。
<?php
$data = $service->getSomeData();
if ($data === null || $data === false) {
$data = array();
}
$client->useSomeData($data);
このコードがまずいのは、ひとつの変数を構築するプロセスが3行に分かれている点だ。ここに手続きプログラミングゆえの記述の曖昧さが入る余地がある。後のメンテナが $data2
も同時に取り扱いたい場合、どう書き足すだろうか。
<?php
$data = $service->getSomeData();
if ($data === null || $data === false) {
$data = array();
}
+ $data2 = $service->getSomeData2();
+ if ($data2 === null || $data2 === false) {
+ $data2 = array();
+ }
$client->useSomeData($data);
+ $client->useSomeData($data2);
<?php
$data = $service->getSomeData();
+ $data2 = $service->getSomeData2();
+
if ($data === null || $data === false) {
$data = array();
}
+ if ($data2 === null || $data2 === false) {
+ $data2 = array();
+ }
$client->useSomeData($data);
+ $client->useSomeData($data2);
前者は取り出しと妥当化をワンセットと認識して書いたもの、後者は、取り出しは取り出し、その後妥当化手続きをすると認識して書いたもの。どちらも間違いとは言えない。意図を知らずにこのソースコードのあるがままを読んだのだと仮定すれば…
(そして、コードの意図を共有しきれないのもまた、保守フェーズの宿命だ)
当初の目的の意味は、レガシーなサービスが提供するデータ形式はこのシステムでは受け入れられないということだ。とすると、前者はまだいいが、後者では $data
変数の値を決定するブロックが分断されている。ここに「文脈を共有しうる手続きが走っている間、妥当化されていない変数が存在する」状態を許しているという問題がある。
では三項演算子に登場してもらおう。三項演算子は手続きを式にすることができる。そして式は文に入れ込むことができる。
<?php
$d = $service->getSomeData();
$data = ($d !== null || $d !== false) ? $d : array();
$client->useSomeData($data);
こう書くことで、$data
は条件付きで変化する状態変数ではなく、必ず再定義されるイミュータブルな変数(とみなして扱うことが可能)であることが、機械的に保証できた。このほうが関数型言語でいう変数(Scalaのval)に近い。
後者の保守プログラマーがなぜそれを定義と手続きとに分けて意識してしまうのか。それは if 文によって $data=
と $data2=
が不揃いになるせいで、変数の定義に見えなくなるせいだ。定義に見えない変数代入は手続きフェーズに移動させるという癖は、C言語経験があるベテランプログラマーほど強くなる。
もう少しPHPらしく。厳密さは型アノテーションとPHPUnitに任せよう。
<?php
$d = $service->getSomeData();
$data = !empty($d) ? $d : array();
$client->useSomeData($data);
PHPの empty
は変数に対してしか使えないので、PHP5.2までは $d
を消すことができなかった。が、さらにここで、PHP5.3で登場したエルビス演算子で置き換えるとどうなるか。
<?php
$data = $service->getSomeData() ?: array();
$client->useSomeData($data);
あらゆる変数から、一時的にですら null
である可能性を完全に排除できた。何より素晴らしいのは、変数の値を決定するのが「1行」になったことだ。変数の妥当化までに他の手続きが介入する余地がいっさいない。
またこうなると、文による記述の制約から開放されるので、機械的なリファクタリングで以下のコードと相互変換できるようになる。
<?php
$client->useSomeData(
$service->getSomeData() ?: array()
);
これを null
を意識しなかった修正前のコードと比べてみるといい。式は値と相互に置き換えられることがポイントだ。
三項演算子はif文と全く等価ではない。それが「式であること」が重要なのだ。
- ミュータビリティを式の中に封印できる
- 変数がイミュータブルになる
- 文は一時変数を欲しがる
- 文は行を消費する
- 複数の行があるところにはコード挿入の余地がある
- 後任の保守プログラマーは何を考えているかわからない
動くだけのコードから、動かなくなる余地のないコードへ、そして、プログラマーの心理学へというのを考えてみた。
三項演算子が無条件に可読性を上げるか下げるかではなく、可読性はプログラマーの心理の中にあり、適切に使うか使わないかを決める能力と権限が必要だという考え方になろう。
特定のイデオムを禁止したせいで、後々本当に読めないコードに変化することのほうが、ちょっとした言い回しを知らない人にマニュアルを参照してもらうより、生産性へのダメージが大きいのは明白だ。