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

Parenscriptで遊んで見る (1) defun編

JavaScript Lisp プログラミング Lisp-Parenscript

TypeScriptやCoffeeScriptといったJavaScriptを吐き出す言語の名前をしばしば聞きます(まだ使ったことはないです…。)が、Common LispにはParenscriptというものがあります。少しいじってみていたのですが、defun周りが気になったので少し遊んでみたという記事です。

次のように、ps環境下でdefunすると、javascriptの関数定義が出力されます。

(defun test-ps ()
  (ps:ps
    (defun f1 (a b)
      (+ a b))
    (f1 10 20)))

(print (test-ps))
=>
function f1(a, b) {
    return a + b;
};
f1(10, 20);

ネストした環境下でdefunを書くというのもなんか気持ち悪いので…、外に出してみます。

; NG
(defun f1 (a b)
  (+ a b))

(defun test-ps ()
  (ps:ps
    (f1 10 20)))

(print (test-ps))
=>
f1(10, 20);

ダメでした。次、psマクロは結局のところ文字列を出力しているのでconcatenateしてみます。見栄えのため改行を補っています。

(defun f1_ ()
  (ps:ps
    (defun f1 (a b)
      (+ a b))))

(defun test-ps ()
  (concatenate 'string
               (f1_)
               "
"
               (ps:ps 
                 (f1 10 20))))

(print (test-ps))
=>
function f1(a, b) {
    return a + b;
};
f1(10, 20);

出力は合いました。パターンが見えてきたらマクロ化するに限ります*1(defun+ps)。

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

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

(defun+ps f1 (a b)
  (+ a b))

(defun+ps f2 (a)
  (+ a (f1 a 20)))

それっぽくなってきました。

defunをもう一つ並べて同名のLisp関数も定義したら便利かもしれない、とおせっかいなことも考えました*2が、body部が必ずしもLisp環境でコンパイルできるわけではないはずなのでやめました。別名で提供するのはありかもしれません。

今度は一々concatenateするのが面倒なのでマクロ化します(with-import-ps-def)。

; (interleave '(1 2 3) "a") => (1 "a" 2 "a" 3 "a")
(defun interleave (lst delim)
  (labels ((rec (result rest)
             (if (null rest)
                 result
                 (rec (append result (list (car rest) delim))
                      (cdr rest)))))
    (rec nil lst)))

(defmacro with-import-ps-def (ps-lst &body body)
  `(concatenate 'string
                ,@(interleave (mapcar (lambda (elem) (list (intern-ub elem)))
                                      ps-lst)
                              "
")
                (ps:ps ,@body)))

(defun test-ps ()
  (with-import-ps-def (f1 f2)
    (f1 10 20)))
=>
function f1(a, b) {
    return a + b;
};
function f2(a) {
    return a + f1(a, 20);
};
f1(10, 20);

関数名を二度(定義とimport)書く必要があるのはいまいちですね…。これを減らそうとするとグローバルに環境を作っていかないとできなさそうです。さらに、JavaScript側での二重定義をどう避けるかと考えるとそう簡単ではないように思います。Parenscriptでマクロを定義するためのdefmacro+psが用意されている一方で、defun+psがないのはこういった辺り*3が原因なのかと思う次第です。

切れ切れになってしまったので、最後に動作するroswellスクリプトを。やめましたと言いつつ、おせっかい版のdefun+psです。main関数内でLisp関数としてf2を使ってみています。

続く?

マクロって楽しいですね Parenscriptいいですね。ドキュメントを見ると、ちょっと変換してみました、ではなくて本気でLispの世界を持ち込もうという意気込みが伝わってきます。とはいえ、Parenscriptを触っていると不満に思うところもあるので、その辺りをいじくり回した結果がいくつか記事になりそうです。

Parenscript関連記事

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


*1:まだまだLisp初心者なので、痛い目見るまではマクロ書きまくるスタンスで突っ走る所存です

*2:外側でdefunする関数名の後ろにアンダーバーを付けているのはこの名残です。実際には同名でも構いません。

*3:グローバルな環境が必要という部分は同じで実際そうなっています。defun+psよりも簡単なのは、マクロによる変換処理はLispの世界で完結するため、JavaScript側へ定義を反映させる必要がないという部分です。