言語ゲーム

とあるエンジニアが嘘ばかり書く日記

Twitter: @propella

論文読み Open, reusable object models http://piumarta.com/pepsi/objmodel.pdf

Viewpoints で開発中のプロジェクトの基本になるアイデアをご紹介します。

オブジェクトモデルというのは、プログラムを書く時に使える値の種類の事です。オブジェクトモデルの異なる言語同士でオブジェクトを共有する方法について書かれてあります。三つの型と四つのメソッドだけで実現可能だそうです。CRL や JVM、parrot と同じたぐいの話では無いかと勝手に予想しています。(追記: ちょっと違いました。一番下の僕の感想参照。)

概要、導入 (p1)

最も単純な実装、例えば lisp のオブジェクトモデルを考えます。lisp では数字、シンボル、リストが使えるとして、lisp オブジェクトの定義はこんな感じです。

enum RecordTag { Number, Symbol, Cons }; /* オブジェクトの種類 */

struct Record {
  enum RecordTag tag;      /* 最初にオブジェクトの種類を指定 */
  union {                  /* 残りはオブジェクト本体 */
    struct Number number;
    struct Symbol symbol;
    struct Cons cons;
  } payload;
};

この方法の欠点。

  • オブジェクト本体の長さが決まっている。
  • 新しい種類を追加したいときはプログラム全体を書き換えないといけない。

これらの問題はオブジェクト指向言語で一般的な物です。例えば、単一継承の言語で複数継承を実現したいと思っても難しい。これらは、ユーザのプログラムからオブジェクトモデル自体をカスタマイズ出来ない事に起因します。この論文では、拡張可能なオブジェクトモデルで問題を解決します。

問題 (p2 - 3)

データ型の追加問題

先ほどのオブジェクトモデルを使った length メソッドを考えます。length メソッドはリストの時にその長さを返し、数字とリストではエラーになります。このメソッドは C 言語で case 文を使って簡単に書けますが、後でデータ型に例えば Vector を追加したい時に問題になります。なぜなら、これではプログラム全体の再コンパイルが必要になり他の人と共有する事が難しいからです。case 文を使わないで、例えばこんな方法でダイナミックにデータ型を追加可能です。また、これをそのまま直接オブジェクト指向言語の基礎に出来ます。

/* 以下で s_ の付く変数名はとあるシンボルを表す。
 * struct vtable と struct object の関係が分かりませんが、
 * 多分後で説明があるのだと思います。
 */
struct vtable *Vector_vt = 0; /* vtable はオブジェクトの振る舞いを表す */

int Vector_length(struct Vector *vector) { /* 新しい Vector 型の length() の定義 */
  return vector->length;
}

void initialise(void) {
  ...
  /* 新しい vtable を作成 */
  Vector_vt = send(vtable, s_allocate, sizeof(struct vtable));
  /* Vector_vt に length メソッドを s_length という名前で追加 */
  send(Vector_vt, s_addMethod, s_length, Vector_length);
  ...
}

int length(struct object *object) {
  /* もしも object が Vector_vt を含んでいたら s_length を通じて Vector_length を呼ぶ */
  return send(object, s_length);
}
単一継承を複数継承に変える例題

以下の例では、このオブジェクトモデルを使って単一継承である pepsi 言語を複数継承に拡張する方法を説明しています。スモールトーカーじゃ無い人には読みにくいですが、本当に興味がある人がいたら他の言語で説明します。

"C1 は Object を継承し、m というメソッドを追加した物"
C1 : Object ()
C1 m [ StdOut nextPutAll: ’this is m’; cr ]

"C1 は Object を継承し、n というメソッドを追加した物"
C2 : Object() "C2 は Object を継承します"
C2 n [ StdOut nextPutAll: ’this is n’; cr ]

"C3 は C1 を継承した物"
C3 : C1 () "C3 inherits from C1"
"C3 は、さらに C2 も継承する(addParent の定義は後述)"
C3 vtable addParent: C2 vtable

"C3 は C1 と C2 の双方を継承するので、m と n の両方使える"
C3 new
m; "inherited from C1"
n "inherited from C2"

実際に複数継承をサポートする addParent: の定義です。

"ParentList は List を継承します"
ParentList : List ()

"vtable に addParent: を追加します。引数は VTable です(多分後述)"
vtable addParent: aVtable
[
  parent isNil "parent が無ければそのまま追加"
    ifTrue: [parent := aVtable]
    ifFalse:
      [parent isParentList
        "parent が ParentList なら ParentList に追加"
        ifTrue: [parent add: aVtable]
        "parent が普通の VTable なら新しく ParentList を作って新旧双方保存"
        ifFalse: [parent := ParentList new
        add: parent;
        add: aVtable;
        yourself]]
]

"メソッド検索を要求されたら、含まれるオブジェクトを全部検索"
ParentList lookup: methodName
[
  | method |
  self do: [:vt |
  (method := vt lookup: methodName) notNil
    ifTrue: [^method].
  ^nil
]

ここまでのまとめ。プログラミング言語のデータの扱い方には色んな選択肢がありますが、その扱い方自体をプログラミング言語から扱う方法です。最初の例が C で、突然独自の pepsi 言語になって少々面食らっています。

開かれたオブジェクトモデル (p3 - p4)

いよいよ本題です。オブジェクトは状態と操作から成りますが、まず操作から説明。一番簡単なモデルを考えます。

  • とあるオブジェクト ? がある。
  • ? に送信出来るメソッドの組を behavior と呼ぶ
  • ? に理解できるメソッドと実装を結びつける表が、オブジェクトの外にある。
  • 他のオブジェクト ?' が、同じ hehavior を共有していても良い。

一つのオブジェクトはメモリ上で次のような形をしています。オブジェクトへのポインタは、vtable を飛ばして状態を指すという風にします(なぜだ???)。

  • object[-1] vtable : behavior を表す
  • object[ 0] 状態

vtable もオブジェクトなので、vtable も vtable を持ちます(vtable vtable と呼びます)。vtable vtable は、全ての vtable がメソッドの実装を検索する時に使う lookup: メソッド(もしくは SymbolLookup とも呼ぶ???)を持たなくてはなりません。vtable vtable もオブジェクトなので、この lookup: をカスタマイズして、オブジェクトの振る舞いを変える事が出来ます。

vtable の中身は、関数ポインタの表でも良いですが、柔軟性の為メソッド名とクロージャ(関数 + データ)の組にしてみます。すると次のような物を実装出来ます。

  • 値を覚えるスロット:
    • 呼び出しメソッドと格納メソッドを実装します(例えば slot と slot:)。
    • slot は、値を持っている。
    • slot は、slot: の値へのポインタを持っていて、値を格納出来る。
    • でもこれでは複数のオブジェクトで vtable を共有出来ないという疑問が沸く。
  • バイトコードや、何か他の言語で書かれたメソッド:
    • メソッド A はバイトコード実行エンジン interp のポインタを持つ。
    • メソッド A は、自分のバイトコードを持つ。
    • メソッド B も、interp を知っている。
    • メソッド B は、自分のバイトコードを持つ。
    • メソッド A と メソッド B は同じ言語で書かれた違うコードを実行出来る。

オブジェクトの本質 (p4 - p5)

最低限必要な本質型を示します。

  • vtable
  • symbol
  • closure

最低限必要な本質メソッドを示します。

  • symbol.intern(string)
    • 文字列からユニークなシンボル(例えば文字列のアドレス)を生成します。
  • vtable.addMethod(symbol, method)
    • vtable にシンボルとメソッドを追加します。
    • すでにシンボルがあれば上書きします。
  • vtable.lookup(symbol)
    • vtable にシンボルがあれば、対応したメソッド(の実装)を返します。
  • vtable.allocate(size)
    • 新しいオブジェクトを生成して、自分を vtable に設定します。
  • vtable.delegated() (必須では無い)
    • 新しいオブジェクトを生成して、parent スロットに自分を設定します(クローンの vtable を設定する)。

オブジェクトモデルの適用 (p5 - p6)

このモデルを基に、実際の言語を、例えば C で作成する方法です。

  • symbol, closure, vtable の構造を決める
  • 四つの本質メソッド (addMethod, lookup, allocate, intgern) の実装
  • メソッド呼び出しの実装(これ自体は本質メソッドに含まれないのか疑問)

このうちメソッド呼び出しの実装を解説しています。例えば send(object, messageName, args ...) と書くと、次のように実行されます。

  • send(vt, SymbolLookup, messageName) を呼んでクロージャ(closure)を取得する。
    • この操作を bind と呼ぶ。
    • vt は vtable つまり定義より object[-1]
    • vt が vtable で messageName が SymbolLookup の時は、vtable vtable の時なので直接 vtable.SymbolLookup の実装を呼ぶ。これが無いといつまでも send の再帰になってしまうから。
  • closure.method(closure, object, args ...) を呼ぶ
    • closure の中に共有データがあるかもしれないので引数に含める

自分の靴紐を引っ張ってオブジェクト世界を飛ぶ方法 (p6)

  • 本質メソッドに使うシンボルを定義しておく。
  • vtable vtable を定義して、その vtable を自分自身にする。
  • vtable.lookup と vtable.addMethod の最初の定義を用意。
  • vtable.allocate を addMethod を使って追加。
  • symbol と closure の vtable を設定。

メソッド呼び出し最適化 (p6 - p7)

毎回 bind と send でメソッドを探さないように最適化する方法について書かれてあります。興味が無いので飛ばします。

評価 (p7 - p8)

この論文に付属するサンプルコードを使って評価をしています。コンパイル後のサイズは多くても 1,602 バイトだそうです。例えば比較的普通の演算に比較して関数呼び出しが多くなるプログラムを使って実験すると、最適化した C と比較して 55.6% の速度(半分に遅くなる)になります。次に、論文の冒頭にある case 文によるメソッド検索と比較すると、一番最適化した状態でこのオブジェクトモデルは 207.0%(倍に速くなる)になります。

あと Javascript と Traits の実装について書いてありますが、特に凄い事は書いてないので飛ばします。制限事項としては以下があります。

  • キャッシュ(新しいメソッド定義や、parent の変更があったら消さないといけない)に強く依存する。
  • コンストラクタを本質メソッドに入れてない(必要無い)があったほうが良いかも。
  • vtable はオブジェクト構造の外(ポインタ - 1)だが、これはいまいちかも。
  • bind や send は、lookup 程簡単にカスタマイズ出来ない(解決法は後述)。

まとめと今後 (p9)

  • シンプルでカスタマイズ可能な意味論(メソッドと実装の結びつき)
  • スタック配置やシグネチャや事前事後条件を vtable のさらに前に置いても面白い。
  • 純粋関数的な性格を持たせる事によって、bind や send のカスタマイズが出来ないだろうか。
  • サンプルコードはとても簡単。四時間 144行で実現出来る。しかも充分にリッチ。

僕の感想

例えば C 言語(や静的なコンパイラ言語)の画期的なところは、スタックやレジスタやヒープをこうやって使えば良いという単純かつ効果的な方法を提示した事だと思います。それに比較してオブジェクト言語のメソッド呼び出しというのは割と複雑なのですが、C の関数呼び出しに比較し得る単純かつベストな方法を提供するというのがこの論文の野望だと思いました。

僕はどちらかと言うと、自然言語でも機械言語でも、ある言語の意味論を他の言語の意味論に完全に翻訳出来るわけがないという思想の持ち主なので、言語によらず一般的なオブジェクトモデルを構築できるというゴール自体に懐疑的なのですが、当たり前に使うようになるとまた気持ちも変わるかも知れません。

細かい部分で納得行かない事もメモります。

  • 例えば、この実装ではデータはオブジェクトではなく、vtable に格納されてしまうのではないか?
  • 「代入」の意味論は? 変数を作成したりアクセスする定義はどこにあるのか?
  • なぜ bind や send のカスタマイズが難しいのかピンと来ない。

最初の疑問の、これは JVM みたいな物か?という件ですが、抽象的なアイデアしか書いてないので何とも言えません。実際の coke & pepsi 言語の動作は VPU に関する別の文書を読む必要があります。