2012年6月4日月曜日

PHPは、未来永劫、駄目言語であり続けると思う

仕事で久しぶりにヤラレましたよ。暗黙の型変換。

$data = ActiveRecord::find($key);
$data->update = true;
$data->save();

たまーに、「stdClassなのでsaveメソッドが無いよ」と言われる。いやあ悩みましたね。

カラクリはこうです。

  1. $dataがnullで戻ってくる
  2. 2行目の代入直前に、$data = (object)$data というキモい型変換をやってのける
  3. 当然saveメソッドなんて有るわけない。
  4. ←いまココ

スカラー同士の型変換はまだいいとして、オブジェクトに変換はイラネーだろと。Zendエンジンにパッチ当てて、型変換を辞めさせようかと真剣に悩みましたよ。でもZendのソースはキモいので、正直触りたくない。

ちなみに逆変換もまた酷いことになります。
  1. $data = (object)ナニカ
  2. $name = "$data";
  3. $data が "Object"とか入ってる。
  4. ←いまココ
PHPの駄目言語たる理由を大別すると、
  • 構文。すなわちZendコンパイラが腐ってる
  • 実行時。すなわちZendエンジンが腐ってる。
    • 主に暗黙の型変換のセンスが悪い。
  • 内蔵ライブラリが腐ってる。
どっちみち腐ってる訳ですが。

構文に関してはかなり致命的です。
  • プログラムのファイル間の結合方法がincludeしかない。requireはincludeの延長線上の機能なのでこれに含みません。
    • 実はinclude文は、バイトコードエンジンの範疇だったりする。故に「実行時インクルード」というキモい挙動。
  • クラスプロパティの区切り文字と、名前空間階層の区切り文字に、別の文字を使ったところ。前者はダブルコロン。後者はバックスラッシュ。なんで?
    • 構文解析がタコいから?
    • 確かにこれ以前までは、バックスラッシュは文字列定数にしか現れません。だからって、記号の質を変えて良いって訳ではない。
    • ディレクトリ区切りにバックスラッシュを使ってるWindowsは、(PHPと同じく)腐ってるわけですが
  • 連想配列の初期化構文。
    • 中括弧を使わないのは、スコープ構文と区別が付かないから?
  • 関数の参照渡しがない。
    • array( & $this, $method )とか、こんなモノ第一級関数とか呼ばないデスヨ。↓後述
    • is_callable("mb_convert_encoding") == true とか馬鹿じゃねえのかと。
  • new演算子の直後で、直接メソッドを使えない。
    • 恐らく、表面上はnewは演算子の体裁をしつつも、実は別系統の構文なのではないか?と勘ぐってますが。ソースは読んでません。
  • use文のキモい用法
    • 名前空間のインポート?
    • 挙句、mix-inのインポート?
  • class定義内で、static変数やconst定数を、変数式で初期化できない。
    • 定数式でなら初期化できます。
    • メンバ変数の初期化構文は、「コンパイラの仕事」だから。
  • 文字列専用の連結演算子が "." 
実行時に関しては
  • 内蔵ライブラリ関数が0とfalseとnullを混在して返すこと。しかもそれぞれ意味が異なる。
    • 世間一般には例外を投げるところです。が、今から変更することは恐らくないでしょう。何故なら、戻り値を===で判定しちゃってるプログラムが山の用にあるからです。
  • 前述の通り、スカラ型から、オブジェクトへの「型変換」は出来るが、出来上がったインスタンスはstdClassで、しかもこれはオブジェクト群の「基底クラス」では「ない」
    • 故に、折角のタイプヒンティングがあるのに「兎に角、任意のオブジェクト」を示す方法がない。stdClassと書くと「無名オブジェクトしか」引っかからない。
      • 自前のクラスを定義する時に、extend stdClassと書けば良いのだが。こんなの馬鹿じゃねえのかと。
    • new stdclassは出来る。
  • includeしかありません。
    • include文は、zendエンジン上で、それに相当するバイトコードがあります。
      • 実は、eval( file_get_contents() ) と同義だったりします。
      • 故に、大規模なプログラム開発には、本質的には向かない。毎回コンパイルしてるので。
        • Java族が言う所の「PHPは遅い」はこの辺りに起因してると思うのですが、PHPのコンパイル自体は意外なほど早かったりします。要はプログラムの規模次第ってことでしょう。
    • この構造故に、class定義の間でincludeを使っても、期待通りには動きません。
  • 第1級関数が無い
    • 5.3では、無名関数のためにClosureオブジェクトというのを内部で使ってるっぽい。create_functionした結果をvar_dumpすると、そう表示する、しかし
      • コンパイラ側にclosureの定義がない。function method1( closure $func1 ) とか書けない。馬鹿じゃねえのかと。
      • 故に、組み込み関数の渡すときは、文字列定数で渡すしかない。
        • $a = "echo" ; is_callable($a) == true とか馬鹿じゃねえのかと。
結論としては、かなり「根深い」ところで「駄目」なので、この「駄目さ」は未来永劫変わらないだろうと予想できます。少なくとも当面、7.xいや8.xぐらいはまだまだ駄目でしょう。大幅な設計変更が必要です。もう「これはもうPHPじゃないね」と言われるぐらいの。

こんな駄目言語でも、仕事では使わざるを得ません。しかし、前述の通りの穴だらけ仕様なので、「安全」というか「バグりにくい」プログラムを書く方法はそんなに多くありません。
  • 関数の戻り値は
    • 常に同じデータ型で返す。しかも成功時のみ。
    • 失敗時は
      • やはりオブジェクト型で返す。例えばデータが見つからなければ 「「データが見つからない」を示すオブジェクト」をnewして返す。
        • 何故って、PHPが別の型に変換しやがった時に、迷宮入りする可能性が低いから
      • throw しまくる
        • データが見つからなければ、「データが見つかりません例外」を投げるべき。
        • 空の配列を返すとかだと、count()==0とかやらなければならなくなる。でもそれは空文字でもヒットするので、実はジワジワ危険だったりする。
          • is_array($result) && count($result)==0 とか馬鹿じゃねーのかと。
かなり駄目かつ、判りにくいですね。まあthrowする方が現実的でしょう。ただし例外を投げても、catchしてもみ消す人もかなり多いので元の木阿弥だったりします。