読者です 読者をやめる 読者になる 読者になる

Parenscriptで少し遊んで見る (5)defstruct編

Lisp Lisp-Parenscript JavaScript

Parenscript(PS)用にdefstructのサブセットを作った話です。例によってParenscript拡張の実験場、ps-experimentプロジェクトで実装を試みています。今回の記事時点のタグblog-play-ps-5をつけています。

github.com

前書き:Parenscript拡張の方針

ここまでと今回の内容を振り返ってみると、大体次のような方針で拡張を進めているようです(後付け)。

  • Common Lispとして書ける部分はそれなりにCommon Lispらしく
    • 理想を言えば、Common Lispコードとしてもそのまま動く
  • JavaScriptべったりなところはむしろよりJavaScriptらしく
    • xx.jsのようなライブラリに依存する部分

前者はdefun編や今回のdefstruct編、後者はドット記法編やキャメルケース編です。前者の「それなり」の範囲は感覚的になんとなく許せない範囲です。

今回で言うと、基本的なデータ構造がJSべったりなのはちょっと…と感じたわけです。

defstruct.psの実装方針

defstructを全面的にカバーするのはオーバースペックなので、defstruct.psの目標を次のように設定しました。

  • 作成時にスロットの初期化ができる
  • 型判定ができる
  • アクセサを生成する
  • 継承ができる
    • スロット名と初期値の継承ができる
    • 子構造体のインスタンスを親構造体のインスタンスとして判定できる
    • 親構造体のアクセサを利用できる

見ての通りdefstructの仕様からすると超のつくサブセットです。まずは想定範囲内では使えそうというところです。

使い方

できあがったものを上記の目標に沿って一通り動かしてみます。出力はprintの横に逐次コメントで書いているので省略します。

展開結果

(print (pse:with-use-ps-pack (:this)))で見られる展開結果のうち、childの部分だけ抜粋。アクセサはPS用のマクロとして別に管理されているため、JSコードとしては見えません。

function child() {
    this.a = 10;
    this.b = null;
    return this.c = 30;
};
function makeChild() {
    var _js4 = arguments.length;
    for (var n3 = 0; n3 < _js4; n3 += 2) {
        switch (arguments[n3]) {
        case 'a':
            a = arguments[n3 + 1];
            break;
        case 'b':
            b = arguments[n3 + 1];
            break;
        case 'c':
            c = arguments[n3 + 1];
        };
    };
    var a = 'undefined' === typeof a ? 10 : a;
    var b;
    var c = 'undefined' === typeof c ? 30 : c;
    var result = new child();
    result.a = a;
    result.b = b;
    result.c = c;
    return result;
};
function childP(obj) {
    return (obj instanceof child);
};
(function () {
    function tempCtor() {
        return null;
    };
    tempCtor.prototype = parent.prototype;
    child.superClass_ = parent.prototype;
    child.prototype = new tempCtor();
    return child.prototype.constructor = child;
})();

Parenscriptのキーワード引数の実装が少々不恰好ですが、現状ではJSコードへのコンパイル段階でどの関数を呼んでいるかを知るすべがないので、止むを得ないというところです。

実装

ps環境下でのdefstruct

一部を切り出し。まずは、(ps:ps (defstruct test (a 10) b))のように、ps環境下でdefstructを利用可能にするためのコード。一番下の(defpsmacro defmacro ...)が本体で、その上に補助関数をずらっと並べています。

見ての通りparse-defstruct-xxxがひたすら並んでいます*1。また、生成されたJavaScriptを見て文法エラーを見つけるのは辛そうなので、パース時のエラー処理を(それなりに)入れています。

JavaScript側の継承を実現する上では「Google流 JavaScript におけるクラス定義の実現方法」を参考にしました。最初はObject.setPrototypeOfを使ったお手軽な方法を使おうと考えていました。一連の流れをOperaChromiumエンジン)のコンソールで再現すると以下のようになります。

> function Parent () { this.a = 10; this.b = 20 };
  undefined
> function Child () { Parent.call(this); this.c = 30; };
  undefined
> Object.setPrototypeOf(Child.prototype, Parent.prototype); 
  Child {}
> var child = new Child();
  undefined
> child instanceof Parent;
  true

が、上記のサイトには互換性に問題ありと書かれていて、どうせ古いIEのことだろうとタカをくくっていたところ、テストに利用しているcl-javascriptで動きませんでした…。ということで、上記のコード(同サイトで紹介されているgoog.inherit実装のParenscript版)になりました。

また、スロット周りの継承を実現するために*ps-struct-slots*というグローバルなハッシュを用意しています。スロットそのものの継承だけであればJavaScriptのcall関数で十分ですが、CLライクなアクセサの定義を行なうために必要になります。

トップレベルでのdefstruct.ps

「使い方」のRoswellスクリプトの様に、トップレベルでPS用の構造体定義を可能にするためのマクロdefstruct.psを生成します。直接には3,4行目が該当のものです。

2015/11/30追記:よろしくないバグを見つけたので修正。あえて古いコードもコメントで残しましたが、On Lispにもある「値を返す他には周囲の世界に影響しようとすべきではない」の原則に反していました。元のコードでは、例えばdefun.psを使ったライブラリxyzを(ql:quickload :xyz)しても、関数が登録されていませんでした。さらに困ったことに、稀に登録されてることがありました…。この辺りの内部的な挙動はまだ理解し切れていませんが。

2015/12/5追記:defstruct.ps内でもregister-defstruct-slotsを呼ばないと上の追記と同じ問題が起こることが分かったのでこのコミットで修正。ちょっと汚いのでどうにかならないものか…。ちなみに、eval-whenは試行錯誤の痕跡でただの消し忘れです。

register-ps-func関数の詳細は第4回参照ですが、PSコードを出力する関数をグローバルに登録するものです。登録した関数はwith-use-ps-pack内(上記実行例参照)で呼び出されてPSコードを出力します(最終的にこのPSコードがJSコードを出力します)。

defstruct.psで構造体hogeを定義すると、hogeを定義するPSコードを出力する関数がこのregister-ps-funcで登録されます。この辺りはdefun.psdefvar.psでも共通のパターンであるため、def-ps-definerとしてマクロ化しました。

できていないもの

  • defstructの機能色々
    • 必要になったものから順次というスタンスです
  • 再定義時の動作
    • 現状は何も対策していないので、例えばスロットを消して再定義すると、消したスロットのゴミ(アクセサ)が残ります
    • HyperSpecを見ると"The consequences of redefining a defstruct structure are undefined." だそうですので非合法ではないですが、SBCLとCCLで試した限りではゴミ掃除ぐらいはしているようなので、踏襲した方が良いだろうと考えてます
      • なお、前者では非互換な再定義だと警告が出ました

Parenscript関連記事

Lisp-Parenscript カテゴリーの記事一覧 - eshamster’s diary

*1:S式≒リストのパースは本当に気軽にできますね