Parenscriptで少し遊んで見る (4)続・defun編

背景

第一回では、ps環境の外側でdefunするためのdefun+psを用意しました。そして、それをまとめてJavaScriptに出力するためのwith-import-ps-defマクロを作ったわけですが、出力する関数名を一々指定する必要がありました。

いったんはそこで妥協したのですが、実際に細かい関数をたくさん書いたときの面倒臭さや、複数ファイル(パッケージ)間での連携をどうするかという部分で実用上すぐに問題になるため、少し見直しました。

方針

以下、defun+ps, defun.psで定義した関数をPS関数と呼ぶことにします。

一つの理想はCommon Lispの柔軟なパッケージシステムを再現することだと思いますが、よく考えるとそこまでするのは無意味ではないかと思えてきます。次のような違いがあるためです。

  • (Common) Lisp: たくさんの関数を定義をしても、export, importするのは一部
  • Parenscript: たくさんのPS関数を定義したら、全てJavaScriptとして書き出す必要がある

第一回で重複書き出しの問題をどう避けるかが難しそうという話を書いたのですが、例えばいっそ定義されたPS関数を全て書き出す機能を用意する(それを何度も呼ぶ間違えはさすがに利用者の責任)、という乱暴な方法でも実用上は十分な解決方法になりえます。

ということで、以下の方針でdefun+psとwith-import-ps-defの改良を考えます。

  • JavaScriptに書き出す対象は大きな単位で指定し、関数単位での柔軟な操作は必要ない
    • 再利用性や今後の拡張性を考えてCommon Lispのパッケージを単位として採用する
    • 実現のために、グローバルな環境にPS関数の情報を持たせる

PS関数を管理する

(defparameter *ps-func-store* (make-hash-table))

(defun register-ps-func (name_)
  (symbol-macrolet ((target-lst (gethash *package* *ps-func-store*)))
    (unless (find name_ target-lst)
       (push name_ target-lst))))

(defun intern-ub (sym)
  (intern (format nil "~A_" (symbol-name sym))))

(defmacro defun+ps (name args &body body)
  (let ((name_ (intern-ub name)))
    (register-ps-func name_)
    `(defun ,name_ ()
       (ps
         (defun ,name ,args
           ,@body)))))

PS関数をグローバルに管理する構造として、*ps-func-store*を用意します。単なるハッシュで、パッケージをキーとし、関数(名)シンボルのリストを値として持ちます*1

実際に登録を担うのがregister-ps-func関数です。パッケージごとに関数名の重複を確認しており、未登録なら登録し、登録済みなら何もしません。これをdefun+psマクロから呼び出すことで、defun+psしたPS関数がそのパッケージに登録されます(要はdefunの場合と似たような動作)。

PS関数をprintする

登録したPS関数をJavaScriptとして吐き出すのがwith-use-ps-packマクロです。

; (interleave '(1 2 3) "a") => (1 "a" 2 "a" 3 "a")
(defun import-ps-funcs (ps-lst ps-body)
  (apply #'concatenate 'string
         (append
          (interleave (mapcar (lambda (elem) (funcall elem))
                              ps-lst)
                      "
")
          (list ps-body))))

(defmacro with-use-ps-pack (pack-sym-lst &body body)
  (with-gensyms (pack-lst func-lst)
    `(let* ((,pack-lst (if (equal (symbol-name (car ',pack-sym-lst)) "ALL")
                           (hash-table-keys *ps-func-store*)
                           (mapcar (lambda (sym)
                                     (let ((name (symbol-name sym)))
                                       (if (equal name "THIS")
                                           ,*package*
                                           (aif (find-package name)
                                                it
                                                (error "There is no package named \"~A\"." name)))))
                                   ',pack-sym-lst)))
            (,func-lst (flatten
                        (mapcar (lambda (pack)
                                  (reverse (gethash pack *ps-func-store*)))
                                ,pack-lst))))
       (import-ps-funcs ,func-lst (ps ,@(replace-dot-in-tree body))))))

次のようにパッケージのリストを指定すると、指定されたパッケージ内の関数を全て吐き出します。なお、特殊なキーワードとして:thisと:allを用意していて、:thisは自パッケージを指し示し、:allは*ps-func-store*で管理している全パッケージを指します。

=>
function f1(a, b) {
    return a + b;
};
function f2(b) {
    return f1(10, b);
};
function f3(a, b) {
    return a - b;
};
function f4(b) {
    return f2(200, b);
};
f1(a, b);

なお、全容は以下のps-experimetプロジェクトにあります。今回のfunction周りのコードはsrc/utils-func.lispです。結構いじりそうなので、一応タグとしてblog-play-ps-4をつけました。

github.com

できていないもの

依存性の登録・管理

依存性を登録しておくと、with-use-ps-packで一々指定しなくても依存するパッケージを自動で指定してくれるような機能です。複数プロジェクトをまたいでの利用を考えると、サブパッケージまで全部指定させるのは現実的ではないため、早めに作っておきたい機能です。

defvarなどもこの機能上に乗せる

JavaScriptに出力するグローバルな構造は一通りこの線上に乗せる必要があります。defvarはこの一例です。defstructはParenscriptに機能自体がないですが、簡易なサブセットぐらいは作って乗せたいです。なお、マクロ定義はJavaScriptとしては見えないので、現状通り別口 (Parenscriptのまま) で問題ありません。

名前空間の分割

PS関数をパッケージごとに管理はしているのですが、今のところJavaScript側にはパッケージ名なしの関数名しか出力しません*2。Parenscriptもパッケージシステムを再現しようとしているため乗っかろうとしたのですが、use-pacakgeやin-packageが望むものでなかった*3ため、とりあえず断念しました。自力で作成するには「Let Over Lambdaのnlet-tailがよく分からなかったのでメモ - eshamster’s diary」でも触れたコードウォークの問題があるため簡単ではなさそうです。必要なら取り組みたいですが、まずはこのままでも割りと実用にはなるかなと。


Parenscript関連記事

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

*1:関数名のシンボルはパッケージ名付きでinternされるので、キーとしてパッケージを持たなくてもいいのですが、速度の上でも利便性の上でもより良いだろうと考えました

*2:このため、現状では他パッケージと関数名が被ったらエラーを出すのが親切かもしれないですが

*3:呼び出すときに必ずパッケージをつける必要がある。コードを見る限り、use-packageやin-packageがPS用に特殊なことをしているようには見えないので、使い方の問題ではないと見ています