メタ言語PythonでPHPのラインタイムをモデル化して理解する

メタ言語としてPythonを使い、PHPの変数やらリファレンスやら周辺の、ランタイム実行モデルを表現してみた。あの変な「リファレンス」ってやつの正体はなんなのか、ってね。今の自分の解釈を書き留めておきたいんだけど、いくら自然言語で説明しても、マニュアルに書いてあるようなわけのわからない話になるだろうから。

PHPにおける値のモデル
class php_val():
    def __init__(self, vartype, data):
        self.vartype = vartype #型
        self.data = data       #実データ
    
    # 書き込み
    def overwrite(self, another):
        self.vartype = another.vartype
        self.data = another.data_deep_copy() #ディープコピーを転写
    
    def data_deep_copy(self):
        #この実装は仮のもの
        #本当はself.vartypeごとに異なる方法でself.dataをディープコピーする
        return self.data
PHPにおける変数名のモデル
# 関数ローカルスコープにおける変数テーブル
var_table = dict()

# 変数の評価がどのように行われるかを表現
# (角括弧が演算子ではなく変数に結合する添字であるという特徴を表現)
def var_entry_of(var_name, key_array=[]):
    var = var_table[var_name]
    for key in key_array:
        if var.vartype != 'Array': raise
        var = var.data[key]
    return var

# PHPにおける代入の意味
def store(left, right_val):
    if not var_table.has_key(left):
        var_table[left] = php_val('NULL', None)
    var_table[left].overwrite(right_val)

# PHPにおけるリファレンス代入の意味
def alias(left, right_val):
    var_table[left] = right_val
実例

PHPコードと対応付けるとこうなる。

<?php
$a =  0;          // store('a', php_val(type='int', data=0))
$a =  $b;         // store('a', var_entry_of('b'))
$a =& $b;         // alias('a', var_entry_of('b'))
$a =  $b[0][1];   // store('a', var_entry_of('b', [0, 1]))
$a =& $b[0][1];   // alias('a', var_entry_of('b', [0, 1]))
// PHPでは $a = (func())[1]; と書けない。なぜなら、var_entry_ofで表現できないから。
unset($a)         // var_table.pop('a')

単体の等号はつねに、相手のdata_deep_copyを自分にoverwriteする。等号の右に&があるときは、変数テーブルvar_tableの割り当てが変更されるだけ。

PHP4とPHP5のオブジェクトの違い

なぜPHP5のオブジェクトはリファンレンス代入していない時も、インスタンスが複製されないのか。

#PHP4
def php4_object():
    return php_val(vartype='Object', data=dict())
    # data=dict()なので、data_deep_copyがdataの深いコピーを作れる

#PHP5
object_pool = []
def php5_object():
    o = dict()
    object_pool.append(o)
    handle = object_pool.index(o)
    return php_val(vartype='Object', data=handle)
    # data_deep_copyでコピーされるのは、プール上のインデックス番号のみ

PHP5形式のオブジェクトをリファレンス代入した場合、通常の代入と違わないように見えてしまうけど、実は違う。どう違うのかについて、このデータ構造モデルで説明できるようになった。


PHPマニュアルの21章にこんな記述がある(今は変わってるのかな)ので、混乱してしまう。

PHP 5 以降、new は自動的にリファレンスを返すようになりました。そのため、この場面で =& を使用することは非推奨となり、 E_STRICT レベルのメッセージが表示されるようになりました。

注意: & 演算子を使用しない場合は、オブジェクトのコピーが 作成されます。クラスの内部で $this を使用した場合、 それはクラスの現在のインスタンスに対する操作を表します。 & のない代入はインスタンス(オブジェクト)のコピーを行い、$this はそのコピーに対する操作を表します。 これはお望みの動作と異なるかもしれません。パフォーマンスやメモリ使用量の 観点から、常に単一のインスタンスに対して操作を行いたくなることもあるでしょう。

ここ、前半で言っているのはPHP5の構文パーサの話で、PHP5の実行モデルの話とはではない。で、後半は、newの受け取りが=&代入でないと、PHP4のオブジェクトが生成直後にディープコピーされてしまう問題を語っているのだと思う。まぎらわしい。


例外条件まみれのマニュアルに書かれた自然言語より、可読性の高い言語で書いたソースコードのほうが、仕様をうまく説明できた。多くの場合には自然言語のほうが受け入れられやすいけど、こっちのほう(ここまでじゃなくても、適切な用語とシンプルな文脈)が理解しやすいような、他の言語をやってきたプログラマがいることに目を向けてもらえたらな。


以下追記。

参考 : PHP以外のほとんどの言語ランタイムに共通するモデル

PHPがいかに仲間はずれな言語かを比べてみると面白い。

# 値インスタンスの管理
class val():
    def __init__(self, vartype='NULL', data=None):
        self.vartype = vartype
        self.data = data

val_pool = [] # 値プール
def new_value(vartipe, data):
    # イミュータブルでプールに等価なものがあれば、インスタンスを再利用
    if is_immutable_type(vartype) and is_equivalent_exists_in(val_pool, vartype, data):
        return reused_instance(val_pool, vartype, data)
    else:
        # 確保した値のインスタンスを返す
        val = val(vartype, data)
        val_pool.append(val)
        pointer = val_pool.index(val)
        return pointer

def value_of(pointer):
    return val_pool[pointer]

# 変数テーブル
class var_table:
    def __init__(self, parent=None):
        self.parent_scope = parent
        self.local_scope_data = dict()
    
    #代入
    def assign_to(self, name, pointer):
        self.local_scope_data[name] = pointer
        # 代替実装としては、変数名未定義の場合に親スコープに委譲することも考えられる
    
    #参照
    def pointer_of(self, name):
        if self.local_scope_data.has_key(name):
            return self.local_scope_data[name]
        else:
            if self.parent_scope:
                return self.parent_scope.pointer_of(name)
            else:
                raise # 変数未定義エラー発生、あるいは 'undefined' 値を返す実装もあり
    

#インデックスアクセスは演算子 (各種演算の出力はポインタ)
def index_access(container_val, key_val):
    if container_val.type == 'Array' and key_val.type == 'int':
        return container_val.data[key_val.data]  # container_val.data:ポインタのリスト key_val.data:整数
    else:
        raise # エラー:非配列オブジェクトへのインデックスアクセス

#                  // local_vars = var_table(parent=global_vars)
#                  //
# $a = 0;          // local_vars.assign('a', new_value('int', 0))
# $a = $b;         // local_vars.assign('a', local_vars.pointer_of('b'))
# $a = $b[0][1];   // tmp = local_vars.pointer_of('b')
#                  // tmp = index_access(value_of(tmp), new_value('int', 0))
#                  // tmp = index_access(value_of(tmp), new_value('int', 1))
#                  // local_vars.assign('a', tmp)
#                  // これなら (func())[0] が可能
# unset($a)        // local_vars.local_scope_data.pop('a')

PHPモデルとの明らかな違いは、もっとも基本的な仕様設計において、「インスタンスへのポインタ」が登場するかしないかだと思う。PHPでは、PHP5オブジェクトになってやっと登場する。

C言語の鬼っ子:ポインタ」について、他の言語はランタイム内部の概念設計にポインタを残し、構文からポインタ記号を排除する努力をした。 PHPだけが、ランタイムからポインタを除去してしまった。そのおかげで、

  • 深いコピーを避けるために、ポインタより難しいリファレンス代入構文を追加する必要が発生した。
  • 結局ポインタ的なものを導入せざるをえず、ダブルスタンダード(基礎の仕様との一貫性がない)になった。ソースコード上はその事実が暗黙的。

…なんていうことに。