いろんな言語のコードを比べてみた
すこし前に、コードの字面について書いたので、いろいろな言語で共通して頻出しそうなパターンを例に、等価なコードを書いてみた。
課題設定
- 配列の各要素を関数に渡すループ手続きを書け。
- 要素は辞書やオブジェクトで、関数は引数の内容を加工するかもしれない。
- ループブロック内のロジック追加を容易にするために、要素を一時変数で参照すること。
いろんな方法論(単位処理をストラテジとして受け付ける集合演算用ライブラリとか?)はあれど、構造化プログラムの延長にあるかぎり、現実的には書き下しに勝る選択肢はないと思うので、言語を問わず比較できるかと思います。分岐だと、書き下しと多態のどっちがいいかは、言語の選択や状況(あと、好みやスキルに起因する感情)によるので。
ややこしいかな?要するに、「公平に言語を比較しようとすると、foreachが得意かどうか比べるのがいいと思う」ってこと。
記述性の評価の比較基準は、
- キーストロークの量を考えて、記述性を評価する
- シフトキーもキーストロークに含んで評価しないといけない
- 英字の連続→記号→記号→英字...の→部分に、脳のシフトキーが要る
- 直前の可読性は次のコードの記述性に影響する
- テキストエディタの単語補完機能も考慮に入れる
といったところだろうか。
可読性については一概に言えない(主観もあるので言いたくない)けど、
- 余計な変数がない
- 文(もしくは行)の数が少ない
- 書く人による「ぶれ」が生じ得ない(誰が書いても同じコードになること。他人の癖は読みにくい)
- 引き起こされる動作が明瞭につかめる
- 発音して読むと、なんとなく自然言語に聞こえる
という特徴は、可読性の高さと関係があると言えるかな。
foreachの元祖の元祖、Perlから順にいきます。
foreach $e (@$data) { &something($e); #$eはハッシュのリファレンス }
for e in data: somethig(e)
data.each do |e| something e end
Ruby スタイル違いもあり。
data.each() {|e|
something(e)
}
for e in data something e end
以上、まったく問題なく比較できるので、ノーコメント。
PHPは事情が複雑。
まず、ありがちな間違いプログラム。
<?php foreach($data as $e) { something(&$e); } ?>
一般的には、これだと仕様を満たしていません。
この構文は、配列要素のコピーを$eに代入します。somethingは要素のコピーを書き換えてしまうため、$dataはまったく変化しません。基本的に、PHPのforeachは(というより、基本がポインタでなくコピーなPHPでは全般的に)破壊操作を想定していないのです。
追記:いまPHP5.2なんですが、関数へのリファレンス渡しが警告されました。関数呼び出しまでにリファレンスを明確に確保するか、リファレンスを受ける関数として定義するか、どちらか明確にせよ、ということなのかな。まず修正すべきは、この書き換えでした。
<?php foreach($data as $v) { $e =& $v; something($e); } ?>
修正にあたって、「歯抜け配列」に対する考慮も必要です。歯抜け配列というのは、
<?php $a = array(1,2,3); unset $a[1]; $a[99] = 100; ?>
のようなものです。PHP配列では、要素数と最大インデックスの関係が保証できません。C言語のfor文イデオムを真似ても、真に等価なコードを保証することはできず、動作結果が入力値の状態に依存します。
PHPには、他のすべての言語で共通している言語処理系の実行イメージの常識が通じないので、コードの等価性を比較するさいは、十分に気をつけてください。
これを前提に、次へ読み進めてください。
PHP4 アイデア1 非破壊操作に置き換え
<?php $tmp = array(); foreach($data as $i=>$val) { $e =& $val; something($e); $tmp[$i] = $e; } $data = $tmp; ?>
PHP4 アイデア2 破壊操作として修正
<?php foreach(array_keys($data) as $i) { $e =& $data[$i]; something($e); unset($e); // 次回の$eへの代入で配列要素を破壊されるのでunset } ?>
PHP5
<?php foreach($data as &$e) { something($e); } unset($e); // この後$eに代入されると、配列の末尾要素が破壊されるのでunset ?>
somethingのパラメータに$eを渡すか&$eを渡すかを書き分けている点に注目してください。関数へのリファレンス渡しが推奨されない理由と、unsetの必要性についてと併せて、(CポインタやPerlのリファレンスより圧倒的に難解で、しかも他の言語と共通しない独特な仕様の)PHPのリファレンスをよく理解していなければ、このような頻出イデオムも、正しく読み書きすることができません。
※私はというと、理解したつもりでもよくバグを作り込んでしまい、そのたびに何度も理解しなおしています。いまだに。
for(var i = 0; i < data.length; i++) { var e = data[i]; something(e); }
よりモダンなJavaScript 1.6 または、相応のライブラリを使った場合
var self = this; data.forEach(function(e){ something(e); // 必要ならクロージャでselfを束縛し、thisと見立てて扱う。 });
OOPと組み合わせるとき、thisのあいまいさ問題のために、どうしてもクロージャの理解が必要になります。thisが暗黙的であることや、クロージャの理解を必要とすることについては是非が分かれると思いますが、私の考え方では、クロージャの理解なくしてモダンなJavaScriptを書けるとは言えないと思います。
さらにモダンなJavaScript 1.7
for(e in data){ something(e); }
ここから型あり言語です。
C言語 配列
/* void **data; を入力とする */ int i; void *e; for(i = 0; i < data_length; i++) { e = data[i]; something(e); }
C言語 線形連鎖リスト
/* typedef struct list_node_tag { void *content, list_node_tag *next; } list_node; list_node *data; を入力とする */ list_node *cur_node; void *e; cur_node = data; while(cur_node->next != NULL) { e = cur_node->content; something(e); cur_node = cur_node->next; }
for(Iterator it = data.iter(); it.hasNext(); ) {
Object e = it.next();
something(e);
}
for(Object e : data) {
something(e);
}
foreach(object e in data) { something(e); }
もっとも最近の型あり言語では、型なし言語と比べて、評価に誤差程度の差しかないことに驚きでした。
これだけで言語の全体を評価できるわけではありません。あくまで、共通してそこそこ頻出するコードを例とした、ソースコードテキストの字面比較でしかありません。ああでも、ごめん、やっぱりまたPHPいじめになってしまった。