JavaScriptはいかにしてprototypeを捨てクラスベース継承を得るのか

きっかけは、prototypeconstructor__proto__ の関係を再確認していたときでした。JavaScriptはこうだけど、これって、AltJSな言語の継承はどうなってるんだろうと思って試したくなりました。

ちょっと気持ち的に、なんだか宗教に入ったみたいに俺は JavaScript がわかったって声高に言う人、だいたいみんな、プロトタイプチェーンによる移譲がクラスベースの継承に束縛されていた思考のブレイクスルーなんだぜ、みたいに言うんだけど、いったいそれがどれほど素晴らしいものなんだろうと考えてしまい...

もしプロトタイプチェーンがそんなに素晴らしいんなら、npm にあるほとんどのライブラリがチェーンを活かして作られてない理由が説明できない。もしかしたら、仕組みがいくら面白くても、実際のメンタルモデルにフィットせず、実は使い物にならないんじゃないか ---- と疑ってみるところから考えなおしてみました。

TypeScript で継承したらこうなりました。

class A {
    public p:number;
    public q:number;
    constructor(n:number) {
        this.p = n;
        this.q = n * 10;
    }
}

class B extends A {
    constructor(n:number) {
        super(n);
        this.p = n * 2;
        // this.q = n * 20;
    }
}

var b = new B(1);
console.log(b.p, b.q);

var __extends = this.__extends || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    __.prototype = b.prototype;
    d.prototype = new __();
};
var A = (function () {
    function A(n) {
        this.p = n;
        this.q = n * 10;
    }
    return A;
})();

var B = (function (_super) {
    __extends(B, _super);
    function B(n) {
        _super.call(this, n);
        this.p = n * 2;
        // this.q = n * 20;
    }
    return B;
})(A);

var b = new B(1);
console.log(b.p, b.q);

なんだこの __extends って。TypeScript は元のコードとわりと対応する JavaScript を吐くってことになってなかったっけ? なんで元のコードと対応しないこんな関数を作らなきゃいけないんだろう。

CoffeeScript ではどうかな?

class A
  constructor: (n)->
    @p = n
    @q = n * 10

class B extends A
  constructor: (n)->
    super(n)
    @p = n * 2
    # @q = n * 20

b = new B(1);
console.log b.p, b.q

var A, B, b,
  __hasProp = {}.hasOwnProperty,
  __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };

A = (function() {
  function A(n) {
    this.p = n;
    this.q = n * 10;
  }

  return A;

})();

B = (function(_super) {
  __extends(B, _super);

  function B(n) {
    B.__super__.constructor.call(this, n);
    this.p = n * 2;
  }

  return B;

})(A);

b = new B(1);

console.log(b.p, b.q);

やっぱり同じようなのができた。親のクラスを参照する方法が動的か静的か違うだけで、ほとんど同じことやってる。

ちょっとまって、JavaScript のプロトタイプチェーンの教科書でこの、n という初期化パラメータを持つオブジェクトの継承はどういうふうに実装したっけ??

function A(n) {
  this.p = n;
  this.q = n * 10;
}

function B(n) {
  this.p = n * 2;
  // this.q = n * 20;
}
B.prototype = new A(0);  // とりあえずオブジェクト要る、けどこれじゃダメだ

var b = new B(1);
console.log(b.p, b.q);
console.log(b.constructor);

おや? 伝統的な JavaScript の教科書的なプロトタイプチェーンだと、new A(0); のところがどうしてもうまく書けないぞ。あとで (new B(1) のときに) 初期化パラメータの値を決めたいのに、最初に何か仮に決めておかないといけない?? で、それだと b.q の値は new B(1) によって決定されるものと食い違う??

もっと不自然なのは、function A(n) { ... } がコールされるタイミング。B.prototype = ...; の時点、つまり何の実体も作られずただ定義だけしたときもうすでに、関数が呼ばれちゃってる。もし親になる A がタイムスタンプ取得やリソースのロックみたいな責務を持っていたら...

と、ここでプロトタイプ信者になった人がいうのは、「クラスベースとは違うのだよクラスベースとは」ってことなんですが、ちょっと落ち着いて、考え方が間違っているっていう前に、TypeScript と CoffeeScript がやっていることは何なのかを、なるべく平易な JavaScript に書き換えて考えてみますね。

function A(n) {
  this.p = n;
  this.q = n * 10;
}

function B(n) {
  A.call(this, n);  // ここで親の初期化プロセスを呼ぶ
  this.p = n * 2;
  // this.q = n * 20;
}
(function() {
  var tmp = function() {  // プロトタイプはパラメータを持ってはいけない
    this.constructor = B;  // __proto__の1段階目にconstructorプロパティがないJS謎仕様への対策
  };
  // hasOwnProperty はちょい割愛
  tmp.prototype = A.prototype;  // BのプロトタイプtmpはAに似た別のダミーオブジェクト
  B.prototype = new tmp();
})();

var b = new B(1);
console.log(b.p, b.q);
  • B のプロトタイプとして new されるのは A ではない
  • それは A に似た別のダミーオブジェクト
  • プロトタイプのコンストラクタはパラメータを持たない (new A(0); の件)
  • 親の初期化プロセスを初期化関数のように呼ぶ必要がある
  • __proto__ の1段階目にconstructorプロパティがないJS謎仕様への対策 ←ひどい

JavaScript でクラスベース継承で得られる恩恵を享受しようと思ったら、ユーザー側でこれだけのコードを書く必要があるんですかね? クラスベースのOOP言語でプロトタイプチェーンの真似をしたいときは、面倒だけどいちいちターゲットに移譲すれば何とかなるというシンプルさだというのに対して、あまりにフェアじゃないんじゃないですかこれ。

うーん。

そろそろプロトタイプチェーンの正義に準じて考えてみましょうか。プロトタイプチェーンは、例えていえばレストランで、無愛想な店長=プロトタイプとのやりとりしかできなかったところに、愛想のいい店員さんが入ってくれて、代わりにもっと愛嬌のある接客をしたり、お客さんのいろんな要望に答えたりしてくれる、みたいなやつですね。その実体は is-a 関係じゃなくてコンポジションによる拡張。なんでも継承で拡張しない、コンポジションを好むべきというプラクティスに合致している気がします。

気がします...

ところが JavaScript の初期設計には Java の圧力があったことを忘れちゃいけません。new Hoge(); だのコンストラクタだの、そもそもそれらは、本質的に必要なんでしょうか? なにか政治的な理由が、言語としての設計の完全性より優先したって可能性はないですか?

で、プロトタイプチェーンやるならこっちね、って最近 ECMA Script 5 で増えた、ナウなヤングに推奨な感じのやつがこれですよ。

Object.create - JavaScript | MDN

Object.create 概要 指定したプロトタイプオブジェクトおよびプロパティを持つ、新たなオブジェクトを生成します。

function Constructor(){}
o = new Constructor();
// これは以下と同じです:
o = Object.create(Constructor.prototype);
// もちろん、実際にコンストラクタ関数の初期化コードがある場合でも、Object.create はそれを反映できません

Polyfill

if (!Object.create) {
  Object.create = function (o) {
    if (arguments.length > 1) {
      throw new Error('Object.create implementation only accepts the first parameter.');
    }
    function F() {}
    F.prototype = o;
    return new F();
  };
}

この代替コードの中でやってることは、AltJS系がクラスベース継承を再現したものをJSに書き下したコードにずいぶん近いですね。コンストラクタは何でもいい、要するにターゲットオブジェクトに変更安全ラッパーをかぶせた何らかのオブジェクトが作れればいいんだ。

...やっぱりね。ユーザーコードに「コンストラクタ」なんて存在、そもそも必要なかったんじゃん。ECMA 曰く、形を定義してから実体を作っていく必要なんてない、と。いきなり実体を持つオブジェクトが存在して、ユーザーはその実体に皮をかぶせたオブジェクトを、好きなときに自由に作ってそれ(=皮)をデコレーションできるよ、コンストラクタ関数の定義なんて要らないよ、というやりかたでOKだそうですよ。Java に言わされてただけなんですっ newconstructor もなかったことにしてください、と...?

ここ、F.prototype = o;oConstructor.prototype ですね。クラスベース再現のほうでは、tmp.prototype = A.prototype; に対応しますね。オブジェクト直接ではなく、あくまで、型があって、その型にプロトタイプオブジェクトが存在するという考え。

プロトタイプチェーンの信奉者は、リアルな値(実際に構造がどうなってるか予測不能)が先にあって、さてこれをどうしよう、という発想なんだけど、これどうなんでしょ? 制限された型(構造は想定の範囲内)が先にあって、それらの関係という抽象概念に対して値を投入する、という発想のほうが、ソフトウェア設計として健全なアイデアなんじゃないかと、自分なら考えますけどね...

たしかに、チェーンの先の値や関数であっても、自分が持っているプロパティのように this.foo とするだけで読み取りアクセスできるのは楽ですが、さて、読み取りアクセスしたもを書き込みアクセス this.foo *= 2; などすると、別の実体ができてしまいますよね。で、またそれを読むと、こんどはプロトタイプのほうにアクセスしない。えーと、これって本当に移譲って言える?

いっぽう、明示的な移譲には何の問題もなかった。さほど面倒でもないし、意図しない操作はターゲットに届かない。

class Operator
  constructor: (@target)->
  exec: ->
    @target.foo *= 2

data = { foo: 1, bar: 10 }

op = new Operator data
op.exec()

console.log data.foo  # =>2

えーと、もしかして JavaScript のプロトタイプチェーンは、オブジェクト間の移譲もマトモに機能していない?

イエス!

ユーザーレベルで使えるプロトタイプチェーンとは、コピーコストをケチったクローンという用途を除いて、あらかじめ定義されたメソッドのセットを引き継ぐしかできない、単一選択のミックスインでしかないのであーる。

あれ...? んーっと、 _.extend(targetObject, myMethodSet) っと...

// Extend a given object with all the properties in passed-in object(s).
_.extend = function(obj) {
  if (!_.isObject(obj)) return obj;
  _.each(slice.call(arguments, 1), function(source) {
    for (var prop in source) {
      obj[prop] = source[prop];
    }
  });
  return obj;
};

どう考えてもこっちのほうがいいですね。何個でも、何回でもいけるもん。そりゃあ Underscore も lodash も大人気なわけだ。

ユーザーの本音「あんなややこしいもん使うぐらいなら、多段な継承なんかなくてもいいよ。他に使える道具は十分揃ってるからなくても我慢できる。使えたとしても、あんなもん手出したら逆に生産性が落ちる」

かくして、JavaScript はクソの役にも立たないプロトタイプチェーンを封印し、すべて関数と単一階層オブジェクトで語れる世界で予定調和を迎えましたとさ。

いっぽうで、この隠されたプロトタイプの秘術を利用してコンパイラを作り、あろうことか邪教であるクラスベースオブジェクト指向JavaScript 内に再現しようとするのが AltJS、という世界の流れなわけです。

いやー、そう考えるしかないでしょこれどう考えても。npm 見ても、AltJS 言語見ても。客観的に。(自分だってそんな結末は期待してなかったけど)

感想

プロトタイプチェーンという「しくみ」の魅力にハマることなく、現実世界でメンタルモデルにフィットし、かつ、実際に使える技術とは何か、ということを見失ってはいけないと思いました。あと、やっぱり JavaScriptJava の政治圧力を受けてだましだましリリースされた、ブラウザマクロのための中途半端なデザインの言語なのだというのは、いまだに重要な視点なんだなと思い出し直しました。

ウィーン ウィーン マサカリ防壁展開中...

保守性・管理性が劇的に上がるScalaのスマートなコードの書き方(ネタ)

最初に断っておくと、Scalaは初心者だからマサカリなんて怖くないもん。というわけで、ScalaになるとPHPではご法度だったいろいろが、どういうふうに解釈できるか考えてみました。

中カッコを省略する

def fizzbuzz(numbers:Seq[Int]) = {
  numbers.map((n) => {
    if (n % 15 == 0) {
      "FizzBuzz"
    } else if (n % 3 == 0) {
      "Fizz"
    } else if (n % 5 == 0) {
      "Buzz"
    } else {
      Integer.toString(n)
    }
  })
}

println(fizzbuzz(1 to 15))

こうやる

def fizzbuzz(numbers:Seq[Int]) = numbers.map((n) =>
  if (n % 15 == 0)
    "FizzBuzz"
  else if (n % 3 == 0)
    "Fizz"
  else if (n % 5 == 0)
    "Buzz"
  else
    Integer.toString(n)
)

println(fizzbuzz(1 to 15))

中カッコを外すことによって、if-else のネストが1本の式で、その中身も単一の式で、関数が単独の if 式であるということがわかるようになります。

中カッコを外してコンパイルエラーが出るようなコード、つまり何かをやるとき2つ以上のステートメントを必要とするコードは、関数型言語的でない状態を持つ可能性があります。状態がなければテストも簡単で、とても保守性アップにつながりますね。

デフォルト引数を使う

def fn(a:Int, b:Int=0) = a + b
fn(1)

便利ですね。ただまあ、これでもいいですが、せっかく型があるなら、個人的にはオーバーロードのほうが嬉しいです。jQueryの引数みたいに真ん中のを省略できる、とかなっちゃっても、型をちゃんと付けてオーバーロードしてれば平気な気がします。

def fn(a:Int, b:Int) = a + b
def fn(a:Int) = a  // fn(a, 0)
fn(1)

グローバル変数

みんな大好きグローバル変数

object TheEarth {
  var population = 600000000
}

TheEarth.population += 100000000
println(s"地球人口はいまや${TheEarth.population}人に")

Scalaのコンパニオンオブジェクトはシングルトンなので、入れ物の時点で完全な実体を持ったグローバル変数なところがJavastaticより優れてるし、PHPみたいにいちいち関数の中で宣言しなくても import で見えるようになります。...こわ

構造化されてない裸の変数だから悪い? 構造化されてようがされてなかろうが、グローバルなものが見えることが悪い? ちがいますね。状態の変化がテストでフォーカスしているインスタンスに閉じないことが悪いんですよね。見えるのはかまわないんですよ。

つまりけっきょく、 できるだけ var はやめろ とくにグローバル変数、ってことですよ。犯人は var ってこと。スコープの広さより先に、状態が変化することが問題。逆に言えば、global でもそれは状態を書き換えない前提なら、そこまで悪くはない。

これがもし val だったら、「式で表現された定数」ってことで、すごくいいですね。グローバル変数なのに。

文字列内に変数を展開する

あ、先やってもうた...

println(s"地球人口はいまや${TheEarth.population}人に")

展開するかしないか s"..." で明示的に分けられるのは、LL系の言語にあまりないメリットですね。

自分を呼ぶ関数を使う

再帰アツいですね。階乗を計算して 5! 求めてみます。

def factorial(n:Int):Int =
  if (n <= 1) 1 else factorial(n - 1) * n

println(factorial(5))

前までの階乗に今の数をかける。5! = 4! * 5 ですね。 でもこれ末尾再帰してないのでスタック使っちゃいます。なんとかしてみた。

def factorial(n:Int):Int = {
  def f(x: Int, s:Int): Int =
    if (x <= 1) x * s else f(x - 1, x * s)
  f(n, 1)
}

これなら末尾再帰しますよ。IntelliJ IDEA ならぐるぐるマークも違うのが出てわかりやすいですね。

末尾再帰しない

末尾再帰する

すごいねー...

... えーと

... ... えーと

def factorial(n:Int):Int = (1 to n).reduce((s, n)=> s * n)

なんかもんくあっか。必要以上に再帰使わなくても、畳み込の方がいいんじゃないですかね。これ末尾再帰と同じ概念の別表現なわけで、ユーザーが意識するスコープが小さくて済む。このへんのメソッドボキャブラリーを増やすほうが、保守性高いコードになると思いますよ。

ちなみにこれ、IntelliJ 先生がこう書いたほうがいいんじゃないかと警告してくれました。

def factorial(n:Int):Int = (1 to n).product

IntelliJ 先生の添削おそるべし。

結論

中括弧なしはダメ、グローバル変数はダメ、なぜって教科書に書いてあったもん、っていうような認識で他人を批判しちゃうのは、ちょっと踏みとどまったほうがいいかも、と思いました。

で、アルゴリズムを組めることがプログラミング技術なんじゃなくて、その言語の一般的なボキャブラリーをどれだけ適切に使えるかが、保守できるプログラムを書く技術なんじゃなかなと思った次第です。なので、やっぱりその、経験すべき通過点を十分通ってないのに人様に講釈たれるのは、違うなーと...

で、まあ、自分も経験はぜんぜん足りないので、それ補うためにも、保守性の高いScalaを書きたければ IntelliJ IDEA を使うのが一番いいです。

あぁ、それと、保守性の高いPHPを書きたいんなら、同じ理由で PhpStorm をすぐに買えばいいと思いますね。

うまれかわるMVC 〜PHPカンファレンス関西2014にむけて

PHPカンファレンス関西2014リレーブログ11人目です。イレブンです。イレブンといえばワールドカップ観戦で忙しいこの時期ですが、みなさんPHPカンファレンスへの心の準備はいかがですか。サッカー疲れでバテないように、テンション上げていきましょう。

先週は、@tbsmcd さんの『機関区 : カンファレンスで起きる何か』で終わっていました。うまれかわったPHPerのエピソード、涙腺がゆるみますね。今年は、もっと多くのビギナーが最後まで楽しめるように、という構成を意識してみました。まだ勉強会慣れしていない人も、いい意味でショックを受けてもらい、うまれかわり感を持って帰ってもらえたらと思います。

さてタイトルの MVCMVC といえばもちろん Microsoft Visual C++ ですよね。ちがいますね。ごめんなさい。いまどきの PHPer にとっては MVC = Mac, Vagrant, Composer ですね。

Mac:

一時期「勉強会のMac率は異常」みたいな言われようしましたが、もういいかげん馴染んできました。勉強会でMacなのはもう当たり前という空気ができています。Macのメリットってなんだろうと考えると、だいたいこんなところでしょうか。

  • アルミ削り出し筐体が硬い = 持ち運んでも安心
  • 他のOSより修飾キーがひとつ多い
  • キーボードが光る
  • Windowsをインストールできる

...そこじゃないでしょというツッコミが聞こえてきたので追記。

  • いきなり bash が起動
  • はじめから ssh 入り
  • はじめから UTF-8
  • サーバと同じコマンドで操作できる
  • PHPRubyで「Windows特有」の影響を受けなくて済む
  • 電源/外部モニタのコネクタが同じ人が多い
  • 使っている人が多くてノウハウが豊富

というわけで、人と同じのが嫌だというオシャレさんは、Mac以外を選ぶといいですね。あれ? 数年前ってたしかこれ逆でしたよね。よくよく考えると、Macのメリットは単に「サーバと共通だから」「開発者間で共通だから」なんですよね。本質的には、Windowsと立場が逆になっただけで、コミュニケーションというか、インターフェースの問題なだけな気がしています、個人的には。

Vagrant:

Vagrantは、歴史は短いのに、あっという間にWeb開発者の共通語になりました。もっとも有名な、OS仮想化のフロントエンドですね。仮想OSだと、別の人のマシンに自分の持っているものと同じ環境設定を再現できる。もう時代は、開発用共用サーバーにFTPじゃないんです。php.ini がちょっとだけ違うせいで半日トラブルを追いかけていたのが無駄、なんてことはなくなりました。

OSを仮想化してしまえば、NginxだろうとPHP-FPMだろうとNode.jsだろうと未知のRubyGemsだろうと、なんだって入れられます。前の環境に戻せなくなるのが怖くて新しい技術を試せない、なんてことはありませんね。

仮に上のMがMacではなくMicrosoftの人だったとしても、開発ターゲットはみんな同じ仮想OSに揃えられるんです。それも、同じやりかた、vagrant コマンドという共通語で。

Composer:

最近のPHPの世界観にもっとも影響を与えたのがこれです。依存ライブラリのインストール自動化。Composerのおかげで、各クラウドベンダーは、PHPのPaaSを同じルールで提供できるようになりました。もちろんPaaSでなくても、ライブラリのバージョン管理が非常に楽になりました。

楽になりました、というと、「あれ? 昔そんなに苦労していた気がしないけどなぁ」と思うかもしれなませんが、そういうことじゃないんですよ。

昔はzipを解凍するだけオールインワン、あっても追加ライブラリ2〜3個、それでいけるのがPHPの強みでした。が、本当は、世界中の開発者の力を少しづつ集めると巨大企業を凌ぐ信頼性を得られるのが、オープンソースの意義ですよね。オールインワンに依存すると、あらゆる課題を1つのプロジェクトに握られる。1プロジェクトの力はさほど大きくありません。

つまり、楽になった結果として、かつて使われていたよりもはるかに多くの外部ライブラリを使うようになり、以前より設計が多クラス化してくるようになったことがポイントです。ずばり、Composerがなかったら、Symfonyコンポーネントがこれほど受け入れられることもなかったでしょうし、ましてや、名前空間の意義も...。もしいまだに、自分のコードがシステムインストールのPEARと競合して泣いていたなんて考えたら、ぞっとしますね。

あ、そういえば、PHPカンファレンス関西2014の基調講演は、「全てを結ぶ力」だそうです。MVC = (Mac,Vagrant,Composer) は、技術そのものではなく、エンジニアを結ぶ力であることが面白いところでしたね。さてじゃあ「全て」とは何なのか ... 気になりますね。

ご参加お待ちしております。

まじめにやってください

(読者の声: あの、そういう話が聞きたくてこのページ開いたんじゃないんですけど...)

えっ? あ、MVC? モデル・ビュー・コントローラ?

---- あれは ... 夜空の星になりました。えぇ

だいたい、なんで MVC すぐ死んでしまうんでしょうね。これからはMOVEだとか、MVVMだとか、新しいこと言うために毎回やられ役です。生き返ったとは聞いた覚えがないのに、またすぐ死んだって言われちゃう。もうね、いいかげん生きてるとか死んでるとか、そういう次元から解き放ってあげませんか。

MVCは人間とそのメンタルモデルに関するものであり、オブザーバパターンに関するものではない。

DCIアーキテクチャ - Trygve Reenskaug and James O. Coplien - Digital Romanticism

ほら、物理的にどういう形をしているか、とは違う次元の概念って、言い出しっぺの人が言ってるじゃないですか。

モデルの変化がビューに通知されようがされまいが、ナントカControllerに明らかにビジネスロジックを書いていようが、いや、もういっそソースコードを見なくても、画面を持つあらゆるアプリケーションには、

  • アプリケーションの本質を構成する部分
  • 人間に見せるために作られている部分
  • コンピューターシステムにそれらを乗せるがためにどうしても必要なコード

この3つがあるでしょって話ですよ。それらがコード中のどこにどう配置されてようが、存在しないなんてことはありえない。

神話や宗教では、そういうのは、人間や動物の仲間ではなくて、精霊とか神とか言われますね。だからお空のお星様でいいんです。どれだけ人間界が汚れようとも、天界かどっかで永遠に生きてる、現世では実体のない存在。人の無意識にあって正義感の拠り所になっている。MVCはいつも人々の心の中に、それでいいじゃないか、と。

MVC、ぼくたちはきみのことを忘れない、永遠に...

フレームワークごとに、いろいろな形で3つのフォルダに入れられて、いつもどこかでdisられていたね、今となってはなつかしい思い出だよ...

f:id:tanakahisateru:20140622021744p:plain

はいっ、濃い人はこんな感じでテンション上げていきますよ〜。

いよいよ本番は今週末、次は @shin1x1 さん、Shin x blog です。お楽しみに。

Yii2.0-beta v.s. Laravel4.1 ベンチマーク

PHP5.5.13のビルトインサーバーで、Yii2.0-betaのDBアクセスを含めた実装をベンチマークテストしてみました。あ、ベンチマークは意味が無いとかいうのはナシです。

HelloWorldベンチだと、ルーティングとビューのオーバーヘッドを比較するしかできません。簡単にチートできてしまいます。データベース接続などのライブラリをプリロードしている方が不利になってしまいます。Yii1は公式発表のHelloWorldベンチがずば抜けて速かった(曰く、ほとんどのコードは必要になるまでロードされないことを表しているらしい)のですが、そういう部分だけを際立たせて、だから全体が速い/遅いと考えるのはおかしいです。

そこで、postとcommentテーブルを持つ同じデータベースに接続して、postデータを1件とそれに付随するコメントをすべて取得する(実際にはデータが1件だけある)処理を含みました。

比較対象は ... Laravel です。

比較したかった理由:

DBクエリの結果がArrayになる系のものは除外します。またDoctrineは、動的なActiveRecordとは動作特性が違いすぎました。同じ種類のもので比較したいとなったとき、Eloquent ORMがすぐに動くのがLaravelでした。

...という理由はそれほどたいした問題ではないです。

比較したかった真の理由:

Laravelは中身にSymfonyコンポーネントを使っていて、いわば二重の構造になっています。さらに、記述姓のために実行時にメタプログラミング的なトリックをしかけています。

Yii2はパフォーマンスを重視して、コア部分にサードパーティーライブラリの層を持たない方針で設計されています。また、IDEでの生産性も重視しており、その結果、PHPの型システムに素直に従ったコードになっています。

で、まあ、Laravelのほうが遅いのはわかっていて、実はポイントは、この作りの違いがPHPコードキャッシュでどれぐらいの差になって現れるのだろうか、というのを知りたかったというわけです。

テストに用いたデータの構造:

CREATE TABLE `post` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) NOT NULL,
  `body` text NOT NULL,
  `created_at` int(11) NOT NULL,
  `updated_at` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `fk_post_category_id` (`category_id`),
  CONSTRAINT `fk_post_category_id` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `comment` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `post_id` int(11) NOT NULL,
  `body` text NOT NULL,
  `created_at` int(11) NOT NULL,
  `updated_at` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `fk_comment_post_id` (`post_id`),
  CONSTRAINT `fk_comment_post_id` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Yii2

コントローラ

<?php
namespace frontend\controllers;

use common\models\Post;

class PostController extends \yii\web\Controller
{
    public function actionBenchmark($id)
    {
        $model = Post::findOne($id);
        if (!$model) {
            throw new NotFoundHttpException();
        }

        $this->layout = 'benchmark-layout';
        return $this->render('benchmark', [
            'model' => $model,
        ]);
    }
}

ビュー

<?php
use yii\helpers\Html;

/**
 * @var yii\web\View $this
 * @var common\models\Post $model
 */
?>
<h1><?= Html::encode($model->title) ?></h1>
<p><?= nl2br(Html::encode($model->body)) ?></p>

<ul>
<?php foreach ($model->comments as $comment): ?>
    <li><?= nl2br(Html::encode($comment->body)) ?></li>
<?php endforeach; ?>
</ul>

レイアウト

<?php
use yii\helpers\Html;

/**
 * @var \yii\web\View $this
 * @var string $content
 */
?>
<?php $this->beginPage() ?>
<!DOCTYPE html>
<html lang="<?= Yii::$app->language ?>">
<head>
    <meta charset="<?= Yii::$app->charset ?>"/>
    <title><?= Html::encode($this->title) ?></title>
    <?php $this->head() ?>
</head>
<body>
<?php $this->beginBody() ?>
    <div class="container">
        <?= $content ?>
    </div>
<?php $this->endBody() ?>
</body>
</html>
<?php $this->endPage() ?>

ActiveRecord

<?php
namespace common\models;

use Yii;
use yii\db\ActiveRecord;

/**
 * This is the model class for table "post".
 *
 * @property integer $id
 * @property string $title
 * @property string $body
 * @property integer $created_at
 * @property integer $updated_at
 *
 * @property Comment[] $comments
 */
class Post extends ActiveRecord
{
    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return 'post';
    }

    // 途中割愛

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getComments()
    {
        return $this->hasMany(Comment::className(), ['post_id' => 'id'])->orderBy(['created_at' => SORT_ASC]);
    }
}
<?php
namespace common\models;

use Yii;
use yii\db\ActiveRecord;

/**
 * This is the model class for table "comment".
 *
 * @property integer $id
 * @property integer $post_id
 * @property string $body
 * @property integer $created_at
 * @property integer $updated_at
 *
 * @property Post $post
 */
class Comment extends ActiveRecord
{
    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return 'comment';
    }

    // 途中割愛

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getPost()
    {
        return $this->hasOne(Post::className(), ['id' => 'post_id']);
    }
}

Laravel4

<?php

use Illuminate\Database\Eloquent\ModelNotFoundException;

class PostController extends Controller
{
    public function benchmark($id)
    {
        try {
            $post = Post::findOrFail($id);
        } catch(ModelNotFoundException $ex) {
            App::abort(404);
        }

        return View::make('post.benchmark', array(
            'post' => $post,
        ));
    }
}

ビュー

@extends('layouts.benchmark-layout')

<?php
/* @var $post Post 手で追加 */
/* @var $comment Comment 手で追加 */
?>

@section('main')

<h1>{{{ $post->title }}}</h1>
<p><?php echo nl2br(htmlspecialchars($post->body, ENT_NOQUOTES, 'UTF-8')) ?></p>
<ul>
    @foreach ($post->comments as $comment)
    <li><?php echo nl2br(htmlspecialchars($comment->body, ENT_NOQUOTES, 'UTF-8')) ?></li>
    @endforeach
</ul>

@stop

レイアウト

<!doctype html>
<html>
    <head>
       <meta charset="utf-8">
       <title></title>
   </head>
    <body>
        <div class="container">
            @yield('main')
        </div>
    </body>
</html>

Eloquent ORM

<?php

/**
 * @property string $title 手で追加
 * @property string $body 手で追加
 */
class Post extends Eloquent
{
    protected $table = 'post';

    public function comments()
    {
        return $this->hasMany('Comment', 'post_id')->orderBy('created_at', 'asc');
    }
}
<?php

/**
 * @property string $body 手で追加
 */
class Comment extends Eloquent
{
    protected $table = 'comment';

    public function comments()
    {
        return $this->belongsTo('Post', 'post_id');
    }
}

コード比較

Yiiのほうがコードが長いですが、大部分は自動的に生成されたコードです。最初はアセットマネージャー、CSRF対策、Flashメッセージなどがあったのですが、それらがベンチマーク結果に影響しないほうがいいと考え、ビューから取り除きました。それでもまだ、ビューにいくつか謎のメソッドコールがありますが、それはフレームワークの機構との関係が強いため、通常外すことがないものです。そこは最低必要なオーバーヘッドだと考えて残しました。

LaravelはIDE用のdocコメントを追加する手間がありました。ide-helper を使ったのですが、hasMany() の先に orderBy() メソッドがないという警告と、App::abort(404); が例外で脱出するのを理解せずに $post 未初期化の可能性の警告が消せませんでした。それを除けば、コード量は十分少なく、やっていることはダイレクトです。

パフォーマンス比較

ともに、PHPビルトインサーバーで動かします。Webサーバが絡む現実のシステム負荷ではなく、純粋にPHPだけの負荷を際立たせたかったので。

OPCache なし/あり で確認。YiiはDB関連のキャッシュを2段階設けました。LaravelのDBログにテーブル定義を見ている様子がなかったので、Yiiはスキーマキャッシュがあるものが標準だと考えるのがよさそうです。接続は4コアCPUなので4本並列で。

siege -c 4 -t 10S -b -q http://localhost:8082/post/benchmark/2

f:id:tanakahisateru:20140616022654p:plain f:id:tanakahisateru:20140616022721p:plain

OPCacheなし

  • Laravel = 10.16 trans/sec
  • Yii チューニングなし = 14.36 trans/sec
  • Yii DBスキーマキャッシュあり = 18.00 trans/sec
  • Yii さらにクエリキャッシュあり = 18.31 trans/sec

OPCacheあり

  • Laravel = 27.63 trans/sec
  • Yii チューニングなし = 47.91 trans/sec
  • Yii DBスキーマキャッシュあり = 69.53 trans/sec
  • Yii さらにクエリキャッシュあり = 72.86 trans/sec

OPCacheの効果

  • Laravel = 2.7倍
  • Yii チューニングなし = 3.3倍
  • Yii DBスキーマキャッシュあり = 3.9倍
  • Yii さらにクエリキャッシュあり = 4.0倍

OPCacheなしで見ると、やっぱり、2層あるLaravelは、1層のYiiに対して、倍は行かないまでも、それなりの遅さになるなぁという差が出ました。これは、Laravelが、コアの堅牢さをサードパーティに委ねることで、上のレイヤーに開発リソースを割けるのがいいという方針で、自覚して進めた結果ですし、それほど驚くところじゃないですね。

それよりも、注目なのはOPCacheの効果です。OPCacheを入れると何倍速くなるか。Laravelが得るものに対して、PHPの型に素直なコードであるYiiのほうが、より多くを得られることがわかります。コードが予測できれば、ランタイム最適化に有利なのかもしれません。

もし将来、PHPのコードキャッシュがJVMやV8のようなネイティブコードレベルの最適化を推し進めたら、Javaでリフレクションがボトルネックになる的な問題にならないかな...?

PHP関西勉強会でYii2-alphaを試しました

「えー、会場の時間の関係でこの後の人は発表時間2分でお願いします」

という消化不良だったので、Yii2を試した感想を書きます。

http://www.yiiframework.com/news/76/yii-2-0-alpha-is-released/

12月のアタマで、Yii2がようやくアルファ版になりました。パブリックプレビューからずいぶん経ちましたね。あと残るはNoSQLのActiveRecordを作っていろいろ仕上げに入るということで、待ち遠しいきょうこの頃、PHP勉強会で「やり残したことをもくもくしよう」というわけで、どこまで進んだのかをじっさいに見てみました。

まず、プロジェクト作成が Composer で簡単にできるようになっていました。

$ php composer.phar create-project --stability=dev yiisoft/yii2-app-basic testapp

単純な構成のアプリケーションはこれ一発で初期化できます。testappというプロジェクトフォルダができて、要るものが全部入っています。設定などの基本構成もほとんど済んでいます。--stability=dev は安定版がリリース されればなくなるでしょう。

で、いきなりPHPのビルトインサーバで動かすと...

$ php -S localhost:8081 -t web

f:id:tanakahisateru:20131222011602p:plain

いきなりBootstrapじゃん! しかも3がベース。

こんなかっこいい画面が出来上がった状態で始まります。最初から問い合わせフォームとログインフォームが付いているのも健在です。

Yii-Bootstrap チュートリアル - tips - ドキュメント - yiijan.org | Yii日本ユーザグループ

ここもう要らないですね。せっせとBootstrapを入れるためにがんばっていたアレをしなくて済むようになるのは大助かりです。

デフォルトのプロジェクト構造も変わりました。

├── assets
├── commands
├── config
├── controllers
├── models
├── runtime
├── tests
├── vendor
├── views
└── web

protected の中身が出てきた感じです。通常の公開フォルダ web がコードの中にいます。もちろんこれは基本形というだけで、yii2-app-basic 以外にも advanced みたいなマルチサイト向けの構成もあります。で、このプロジェクトのルートが名前空間\app になります。ビューも静的ファイルも上に名前空間のルートがある感じなので、アプリケーションが綺麗にパッケージになってる感じがありますね。

注目すべきは画面の下の方にあるデバッグバーです。Symfonyにあるアレです。リクエストごとの状態やログを見られる機能。面白いのはPHPのバージョン番号が書いてあるところ。

f:id:tanakahisateru:20131222013830p:plain

クリックするとアレがそのまんま!!

f:id:tanakahisateru:20131222013915p:plain

phpinfo(); exit; だけやるアクションをいちいち書かなくてもいいんですよ。わかってらっしゃる。

f:id:tanakahisateru:20131222014310p:plain

Giiも乗りました。GiiはWebブラウザでコード生成できるツールです。

モデルとCRUDを作ってみます。モデルはこれまでと全く同じ感じで作れました。CRUD名前空間をフルに指定する必要がありました。リリースまでにもうちょっと補完が効くようになるんだろうな。

f:id:tanakahisateru:20131222014752p:plain

f:id:tanakahisateru:20131222014804p:plain

レイアウトは main.php 一択になり、2カラムレイアウト用のアクションのメニューはない感じになっています。Yiiの1のときの2カラムレイアウトとアクションメニューって、レスポンシブでモバイルファーストにするとき標準とするにはちょっと邪魔で、自分は外して使っていたんですが、それが基本になったということで、個人的には嬉しい仕様です。

そうそうレスポンシブといえば、Bootstrap3ベースなので何もしなくてもこのぐらいできてます。本当に自動で作ってまだ何もしてないサイトですよ。

f:id:tanakahisateru:20131222015324p:plain

ログインの認証のカスタマイズもわかりやすくなっていました。Yii 1 ではこういうコンポーネントを実装しました。

<?php
class UserIdentity extends CUserIdentity
{
    public function authenticate()
    {
        // ここを書き換えてDBアクセスによる認証など自由に書く
    }
}

Yii 2 はこうです。

<?php
namespace app\models;

use yii\web\IdentityInterface;

class User extends Model implements IdentityInterface
{
    // IdentityInterface を実装する
}

こういう形の User という名前の モデルmodels に置いてあります。それをこう。

<?php
namespace app\models;

use yii\db\ActiveRecord;
use yii\web\IdentityInterface;

class User extends ActiveRecord implements IdentityInterface
{
    public static function tableName()
    {
        return 'user';
    }

    public static function findIdentity($id)
    {
        return self::find($id);
    }

    public static function findByUsername($username)
    {
        return self::find()->where(
            'username=:name', [ ':name'=>$username ]
        )->one();
    }
    // あとは初期実装のまま
}

Model を ActiveRecord にして、必須であるテーブル名とその検索方法を書いたぐらい。

これまでは DB 上でアカウント情報を表すARなモデルを作って、それを CUserIdentity が使うという形で、まあそれでも何でもできたんですが、Yii 2 ではもっと簡単に、モデルとして見えるところにユーザが最初から定義されていて、それをARにするならどうぞ、というわかりやすい感じになっています。

で、ここでもうひとつ驚きがありますね。->model() がなくてクラス自体がリポジトリになってますね。Rails感が増しました。また、findByAttributes()find() + CDbCriteria のどっち使うかがなくなり、引数なしの find() の戻り値がクエリビルダになるからみんな使え、という仕様になりました。

対比してわかりやすいよう Yii1 のコード例を3つ:

User::model()->findAllByAttributes(array(
    'username'=>$username,
));
User::model()->find('username=:name', array(
    ':name' => $username,
));
$criteria = new CDbCriteria();
$criteria->addCondition('username=:name');
$criteria->params = array(
    ':name' => $username,
);
User::model()->find($criteria);

どれ使えと... というのがひとつの様式に統合された感じです。

で、どの形でフェッチするのか、つまり、ARとしてoneか、ARの配列としてallか、単一の値をscalarか、というのを普通に書くというのも、例には出してないですが、CDbCommandの方法との一貫性につながっています。

まああと、PHP5.4以上なので普通にショートアレイが使われていますね。コンパクトでいい感じです。

コントローラまわりの改善。不自然さはなくなっていて、依存関係的に正しい感じになっていると感じました。Yii1は、アクションのアクセス制限の仕組みの根本は付け替え可能なフィルタ機能なのに、なぜかコントローラの抽象にaccessRulesなんてメソッドが最初からある、なんていう、現実的だけどちょっと変な進化をしてきた痕跡がありましたが、そこらへんたぶんだいぶ整理されてます。

ビューも、ビューファイルでいう $this が実はコントローラのことだった、なんてことはなく、yii\web\Viewインスタンスになっています。これまで可能だった、ビューヘルパーとしてコントローラにメソッドを追加、なんていう変な作り方はできなくなっています。

あと、これ大きいなと思うのが、エンティティとフォームを兼用しているあのARモデルですが、フォームっぽさがCRUD用途のみという感じになるように、Giiが生成するようになったことです。検索フォームなどDBの列とは別のプロパティが必要な種類のフォームは、エンティティのCRUDをやるためのモデルから外れて、検索だけやるフォームとしてARではない単独のフォームモデルに分離されるようになりました。

これ教育上とてもいいことだと思います。追加機能のためのフィールドをどんどんひとつのモデルに付け足して混在させていく作りって、モデル駆動の人にしてみたら、中心とするモデル実装の汚染なんですよね。でもプログラマーは、お手本コードに search() なんていう特殊なものを同じところに書いてあるなら、それ真似するでしょ。結果、どんどんひとつのモデルにフィールドとメソッドが増え、太っていきます。ファットモデル。Yii2の初期コードは、それをしないお手本になっている気がしました。(それでいてフォームバリデーションを最低限必要なぶんだけ同じ所に持っているモデルというのは、素早いプロトタイピングをやるのにすごく便利です)

Yii経験者じゃないと何のことかわからないと思いますが、とにかくYii2はよくできています。速さと便利さのためにいくらか犠牲になっていた、正しい方法への誘導というフレームワークの意義を、可能な限り取り戻そうという意図が感じられました。数年後、PHP界の正しいRailsとして選択の余地なしという評価をもらえるものになっていると思います。たぶんね。

PHPでは配列ではなくオブジェクトに状態を持たせよ

アドベントカレンダーを書いたらコメントに面白い課題もらいました。

Python - すごく簡単なアルゴリズムがphpで書けなくてつらい」のやつ。

id:methane

php の array と参照の関係がクソで無いなら、 http://qiita.com/methane/items/41e1376c41d8c15e8894 これを普通に書いてみてください。

id:tanakahisateru

面白そう。やりましょう。

最近ずいぶんPHP成分多めですが、実はPythonも好物なのでホクホクです。

といっても、あのエントリーは「php の array と参照の関係がクソで無い」とは言ってなくて、むしろ逆にそこは腐ってるから避けろ、オブジェクトで囲んでやれ、という話だったので...(^^ そのままやってもPythonの性能にはならないとわかっているので、配列を直接使うのはイヤです。なので、オブジェクトが状態を持つように書けという主張がどういうことなのか、実装で表してみたいと思います。

あ、5.5 の yield 使ってます。せっかく PHP にも Python のあれができたので。 yield なんてズルいと思う人はそういうイテレータークラスを実装したらいいと思います。イテレーターの実装が面倒な人は Ginq とか検討してもいいかもです。

<?php
/**
 * 末尾から消化できるシーケンス
 */
class TailConsumableSequence
{
    protected $sequence;

    function __construct($source)
    {
        $this->sequence = $source;
    }

    /**
     * Pythonのpopと同じようなもの
     * 末尾からゆらぎぶんさかのぼって抜き出す
     */
    function consume($fluctuation=0)
    {
        $size = count($this->sequence);
        $targetPos = $size - 1 - $fluctuation;
        $consumedValue = $this->sequence[$targetPos];
        for ($i = $targetPos; $i < $size - 1; ++$i) {
            $this->sequence[$i] = $this->sequence[$i + 1];
        }
        array_pop($this->sequence);
        return $consumedValue;
    }

    /**
     * n以上残っているか
     */
    function remainsEqualsOrMoreThan($n)
    {
        return count($this->sequence) >= $n;
    }

    /**
     * 残り数かlimitかどちらか小さい方
     */
    function sizeOr($limit)
    {
        $size = count($this->sequence);
        return min($limit, $size);
    }
}

/**
 * 数列の後ろのほうからごにょごにょ拾ってきてペアを作る
 * ごにょごにょ = 最末尾とそこから limit までの範囲さかのぼった位置のペア
 */
function allRandomPeairsFrom($sequence, $limit=100)
{
    $tcs = new TailConsumableSequence($sequence);
    while ($tcs->remainsEqualsOrMoreThan(2)) {
        $a = $tcs->consume(); // 最末尾から取る
        $b = $tcs->consume(
            mt_rand(0, $tcs->sizeOr($limit) - 1)
        ); // 最末尾からある程度さかのぼって取る
        yield [$a, $b];
    }
}

function test($n, $r) {
    foreach (allRandomPeairsFrom(range(0, $n-1), $r) as list($a, $b)) {
        //printf("%d %d\n", $a, $b);
        assert($a - $b < $r * 2);
    }
}

test($argv[1], 10);

これでいかがでしょうか。追記3のベタ書きと同じような特性なのに、わりと読めるコードでもあります。

$ time php matching2.php 10000; time php matching2.php 20000
php matching2.php 10000  0.23s user 0.01s system 99% cpu 0.245 total
php matching2.php 20000  0.40s user 0.02s system 98% cpu 0.421 total

追記2 で list_pop() にしていた処理をベタ書きしたら、やっと予想通りの性能が出るようになった。 php で配列の参照に対して操作してはいけない。つまり、配列を操作するアルゴリズムを関数にしてはいけない。

すごく同意。関数にしてはいけない。配列の参照に対して操作してはいけない。生で配列をごりごり書き換えるとポインタが使えないためにリファクタできなくなってハマる。じゃあどうするか...

PHPで状態を持つバッファは、オブジェクトで囲むといいです。参照渡しとか配列のコピーとかから来るややこしさから解放され、ベタ書きしたのと同じような動きになります。オブジェクトはポインタなので、好きなだけ関数に渡してもよくて、もっといいのは、オブジェクトのフィールドはスコープを行き来するローカル変数より、もっと安定したメモリに置いてあるってことです。

あの「状態の変化は、それを意図したメソッドを持つオブジェクトでのみ起こるべきです。」というのは、お作法の話であるという側面もあるけど、他に、そう書いたほうが参照渡しよりも問題が簡単になって、がんばらなくてもパフォーマンスが安定する、という意味でもあるんです。

まわりくどくて重厚に見える方法のほうが実は素直、というのが最近のPHPの面白いところです。OOPのお作法がよくなるのと、物理的に悪いものから遠ざかるのが反比例じゃない感じ。

PHPのarrayはPythonでいうところの名前付きタプルとして使うぐらいが、いちばんいい使い方だと思っています。

PHPが糞言語なのはどう考えても参照をポインタだと思っているお前らが悪い

この投稿はPHP Advent Calendar 2013の12日目の記事です。

PHP恒例行事の参照と三項演算子のdisりですが、そろそろあさってな議論はやめませんかという話です。

今年のPHP-dis大賞といえばこちら。

PHPとかいう糞言語|いんまのブログ

※ 追記: これ書かれたのは2012年でしたすんません。

なんで君たちそんなコードが必要なのかね、と。結論から先言うと、きみたちがPHPが使えないって思うのは、そんな挙動に左右されるようなコードを書くからでしょ、だからCとかRubyとかそういう簡単な言語でわかった気になっている初心者はまったくもう...というわけでPHPの言語文法の基礎んとこ、いきますね。

まず、PHPのarrayは「値」です。もちろん文字列も「値」です。値は値なんだけど、それはミュータブルです。PHPのarrayもしくは文字列の代入は、一見すると、ポインタを使わない大きなC構造体を代入するような感じになります。

function x2first($arr)
{
    $arr[0] *= 2;
    return $arr;
}

$input = array(1, 2, 3, 4, 5);
$output = x2first($input);

// PHPの配列渡しはポインタではない
assert($input[0] != $output[0]);

PHPでは、コール先で中身を変更しても、コール元のスコープでは変数の値が維持されています。

で、ここでポインタわかって調子乗っちゃってるプログラマーは誤解するのですよ。大きな配列や文字列を別の変数に代入したり、関数に渡したりすると、その回数ぶん メモリの確保とバイナリのコピーが起こっている と。

その勘違いに捕らわれた人は、C++の参照演算子(ポインタを逆に表現した感じのアレ)を思い出して、「そうだPHPでも参照を使えばポインタと同じだ」と思い込んで、まあこんなコードを試すわけです。

function x2first_ref(&$arr)
{
    $arr[0] *= 2;
}

$input = array(1, 2, 3, 4, 5);
x2first_ref($input);
// 引数がコピーだということは、ポインタ相当のパフォーマンスを得るには参照渡しかな
assert($input[0] == 2);

やったーこれでメモリのコピー量がスタックに8バイト積むだけで済むぜ。

んなわけねーよ。

まあ、それほど極端でなかったとしても、みなさん計測せずに感情的な判断をしていませんか? パフォーマンスのために書いたつもりが、計測してないなんて、そんなの一度も実行してないのにバグってないロジックができたと言い張ってるようなものですよ。

ちゃんと計測します。

function profile($funcname)
{
    $bigarray = range(1, 1000000);

    echo $funcname . "\n";

    if ($funcname) {
        $start = microtime(true);
        $ret = $funcname($bigarray, 500000);
        $end = microtime(true);
    } else {
        $start = microtime(true);
        $end = microtime(true);
    }

    echo "  caller memory: " . number_format(memory_get_usage()) . "\n";
    echo "  time: " . (($end - $start) * 1000) . "(ms)\n";
    unset($bigarray);
    unset($ret);
}

ob_start();
profile(null); // ウォームアップ
ob_end_clean();

誰でも試せる素朴な計測ツールです。zval_とかわからなくても大丈夫な感じのやつ。100万要素の配列を準備して、処理にかかった時間と、呼び出し元スコープでの使用メモリを報告します。

まずは、本当に何もしない場合の数値を基準として取っておきましょうか。

profile(null); // 関数を呼ばない場合

// caller memory: 144,639,024
// time: 0.0030994415283203(ms)

実行するときは、たぶん php.ini のメモリ上限にひっかかるので、上限外してやりましょう。

$ php -d memory_limit=-1 test.php

さてじゃあ、引数渡しと戻り値の返却を、参照ありなしいろいろ試してみましょう。

// 参照なし
function nop_noref($arr)
{
    echo "  callee memory: " . number_format(memory_get_usage()) . "\n";
    return $arr;
}

// 参照渡しのみ
function nop_ref_arg(&$arr)
{
    echo "  callee memory: " . number_format(memory_get_usage()) . "\n";
    return $arr;
}

// 参照渡し+参照返し
function &nop_ref_both(&$arr)
{
    echo "  callee memory: " . number_format(memory_get_usage()) . "\n";
    return $arr;
}

profile('nop_noref');
profile('nop_ref_arg');
profile('nop_ref_both');

// nop_noref
//   callee memory: 144,639,072
//   caller memory: 144,639,072
//   time: 0.028133392333984(ms)

// nop_ref_arg
//   callee memory: 144,639,128
//   caller memory: 241,027,888 ← コール後にメモリ増大  
//   time: 106.05406761169(ms) ← なんだこれは

// nop_ref_both
//   callee memory: 144,639,112
//   caller memory: 144,639,112
//   time: 0.034093856811523(ms)

おい参照渡し、参照しない場合に対して処理時間が3700倍とかどないなっとんじゃい。

参照渡しのみした場合、見事にメモリが倍になっています。つまり巨大配列のコピーが発生した証拠です。実はこれ、参照返しをしなければ、戻り値の受け取りのとき、配列のコピーが生まれているのです。参照渡しだけでなく参照返しも意識しないと、こんなふうに内容が同じ100万要素の配列が2つできちゃう。こいつは厄介...

いや、厄介じゃないですね。驚くべきことに、いっさい参照を意識していないコールが最も優秀になっていますね。安全だし速いし。パフォーマンスのために内容破壊のリスクを冒して参照渡しするべき、とか考えてた人は残念でした。その努力には、まったく意味がありません。

いちど構造が作られてしまったら、多くの場合は読み取りアクセスだけで事足ります。読み取りに関しては、参照渡しだろうとそうでなかろうと、その負荷はまったく同じです。(7マイクロ秒の差は計測誤差です)

function getat_noref($arr, $index)
{
    echo "  callee memory: " . number_format(memory_get_usage()) . "\n";
    return $arr[$index];
}

function getat_ref(&$arr, $index)
{
    echo "  callee memory: " . number_format(memory_get_usage()) . "\n";
    return $arr[$index];
}

profile('getat_noref');
profile('getat_ref');

// getat_noref
//   callee memory: 144,639,168
//   caller memory: 144,639,168
//   time: 0.03814697265625(ms)

// getat_ref
//   callee memory: 144,639,232
//   caller memory: 144,639,232
//   time: 0.030994415283203(ms)

参照にパフォーマンス上の意味はない、つまり、参照記号の & は、コール元に魔の手を延ばして値を書き換えてやるぞ〜、と待ち構えている怖いヤツの目印だというだけなんですよ。

もういちどポインタを思い出して。そう、PHPの変数は、最初からすべてポインタなのです。だから特別な記号を使わなくても、いくらでも変数を関数に引き渡せるのです。いやそうとしか説明できないでしょ、この結果見たら。

PHPで、すごく層の厚いフレームワークが案外実用的な速度で動く理由は、実はこのへんが効いているんですね。リクエストごとにやり直しでありながらも、言語ランタイムで代入が自動的にすべてポインタなので、層の間で無駄なコピーが発生していない。だからこそ、大きなarrayをコンフィグとして取り回したり、設計をあれだけ高級化しても案外ダメージが少ない。

でもまって、中で変更しても外は保護されてたアレはどう考えてもコピーでしょ。----そうですね、アレを試さなきゃ。

function x2at_noref($arr, $index)
{
    echo "  callee memory1: " . number_format(memory_get_usage()) . "\n";
    $arr[$index] *= 2;
    echo "  callee memory2: " . number_format(memory_get_usage()) . "\n";
    return $arr;
}

function &x2at_ref(&$arr, $index)
{
    echo "  callee memory1: " . number_format(memory_get_usage()) . "\n";
    $arr[$index] *= 2;
    echo "  callee memory2: " . number_format(memory_get_usage()) . "\n";
    return $arr;
}

profile('x2at_noref');
profile('x2at_ref');

// x2at_noref
//   callee memory1: 144,639,360
//   callee memory2: 241,028,168 ← ここでメモリを大幅に確保
//   caller memory: 241,028,168
//   time: 111.29093170166(ms) ← コピーコスト

// x2at_ref
//   callee memory1: 144,639,376
//   callee memory2: 144,639,376
//   caller memory: 144,639,376
//   time: 0.036954879760742(ms)

最初の nop_noref() に対して、この x2at_noref() がちょうど、中身を操作するかしないかの違いになっています。中身を操作しなければポインタのままだったものが、操作したことによって実体を共有できなくなると、こんどは裏で勝手にクローンが作られる、これがPHPの「普通の変数」の正体です。なんという高級言語。わざわざそういう最適化を書かなくても、言語ランタイムが暗黙的にやってくれてるんですよ。

こう見ると、参照のほうがむしろ普通に見えてきます。ただまあ、PHPの参照はいちど変数が参照になってしまうと、二度ともとに戻ることができないので、扱いにくくてやっぱりダメです。我々アプリケーションプログラマーにとっては、より平易で少ないコード量で、より安全で最適化された処理を得るのが正義なんですから。

zval の is_ref がどうとかあたりのちゃんとした説明は、2013-03-07 - bravewood の日記 で読めます。中身にこだわる方はどうぞ。参照代入演算子は、右辺の変数の特性をこっそり変化させてるあたりで、「うわなにこれ演算子の見た目に騙されてた」感を堪能できますよ。

まあ、そもそも話で、LLな言語の変数がミュータブルなのはしょうがないですが、であるからこそ、別のスコープではできるだけイミュータブルな値であるように意識して扱うのが、うまいプログラムのお作法ですよね。どこで中身が書き換えられるかわからない、副作用を期待した作りではなく、生成は生成に関わる箇所だけで、読み取りは参照透過で途中で言ってること変わらない、となっているのが、言語を問わず良いとされる方法です。

状態の変化は、それを意図したメソッドを持つオブジェクトでのみ起こるべきです。コードの中にできるだけ状態を作らない。わかりやすいインターフェースのオブジェクト設計で、これは状態だ、他は状態じゃないと静的に判別できるように考えましょう。今のPHPのオブジェクトインスタンスは、Javaのそれと同じく、明示的なクローンをするまで実体はひとつです。arrayを直接ではなく、可変な変数を持つオブジェクトを用意して、オブジェクトを普通に関数に渡し(オブジェクトは素直にポインタです)、そのオブジェクトのメソッドを使って変化を管理しましょう。

というわけで、参照渡しをカジュアルにやるのが間違いなのです。関数の戻り値の型の整合性がとれず、やむなく出力引数で表さなければならない場合などを除いて、基本的には使わない。使う意味がない。参照の仕様から来る複雑さに関しては、PHPが悪いというより、基礎を押さえずに用途を勘違いして使うほうが悪いと思います。PHPの変数の基礎を知っていれば、ほとんどの場合使わなくていいということが、おのずとわかると思います。

PHPが変な言語に見えるのは、そういう特殊な高級言語だと知らずに、素朴なメモリモデルを持つCのようなもので例えようとするのが良くないのです。もちろん、本当のビギナーが誤解して使っている場合も多いですが、よりややこしいのは、少々わかったふうな若葉マーク取れたぐらいの人が起こす勘違いです。そこに門外漢が弱いものいじめのように集まってきてしまう。本当のPHPプログラマーは、これがどういう言語なのかをよく知って、つつましく適切に、でも都合のいいところを活かして便利に使うのです。

まったく役に立たないかに見える参照の機能ですが、僕は、ここだけは使えるというホットスポットがあると思ってて、あとはそれを紹介しておきますね。

$str = '123-456';
preg_match('/^(.*?)-(.*?)/', $str, $match);

preg_match()の第三引数は参照渡しです。このコールで、$matchは宣言されている必要がありません。このように、コール元のスコープの変数の代入式と同じように働く参照渡しは、時として役に立ちます。

あと、クロージャが束縛する変数。

$removed = array();
$data = array_filter($data, function($element) use(&$removed) {
    if ($element->ckeck()) {
        return true;
    } else {
        $removed[] = $element;
        return false;
    }
});

$removed の参照を束縛しています。ここがもし参照でなかったら、まさに引数に配列を渡したときのように、クローンに対して操作され、$removed=array(); が維持されてしまいます。本来はオブジェクトを設けるべきなのかもしれませんが、クロージャがインラインで複雑さをさっと閉じ込めてくれることを思うと、こういう場合は、素朴なforeachループルーチンと置換えられるような手軽さが合っています。

個人的には、参照がありがたいと思って使うのはこれぐらいです。他に使い道ってあまりないなぁ。

あ、そうそう、参照を使ったら、忘れずに unset() するか、すぐにそのスコープを抜けること。変数がいちど参照になると、同じ名前でその変数名を再利用することができないのです。以後そこに代入するのは、参照先の領域への書き込みという意味になってしまい、新たな値を持つ変数を作るという意味にはなりません。

えーと、PHPが糞言語なのはどう考えても参照をポインタだと思っているお前らが悪いって言ってごめんなさいなので、歌って気分を晴らしてください

"仕様が理解らないの〜 なぜだどうしてだ〜 アホかー"

参考: http://www.youtube.com/watch?v=fZ-CM7n3F5c

明日は @ockeghem こと徳丸先生です。楽しみですね。