y2q_actionman’s ゴミクズチラ裏

内向きのメモ書きを置いてます

Parenscript を使ってみた

ちょっとしたおもちゃを作るために Parenscript を使ってみたので忘れないうちに感想を書いておく。

github.com

経緯

上のプログラムは、元々は Common Lisp でちょっとした計算を行うものだった。 UI としては REPL をそのまま使っていた。 そのうち、 Lisp 処理系を起動しなくても使いたいとか、スマホでも簡単に見られるようにしたいなどと思いはじめた。そこで、以下の手順で公開しようと思った。

  1. ここまで書いた Lisp コードを ParenscriptJavaScript に変換する。
  2. 変換した JavaScript を呼び出す UI を HTML + CSS + JS で作る。
  3. DOM を触るコードは JavaScript で手書き。
    • DOM 操作を Common Lisp で書いて Parenscript で変換して JavaScript を生成することも出来るのだが、そうしなかった。 DOM 操作は SBCL の REPL でデバッグできるようなものではなく、ブラウザの console で扱う方が簡単。そのため、DOM 操作については JavaScript を直接書く方が楽だと判断した。
  4. Github pages で公開

上記のような経緯で Parenscript を触った感想を以下にまとめる。

最新の構文に対応していない

現在 (2021-3-14) 時点で Quicklisp から拾える Parenscript のバージョンは 2.7 だが、これが出力する JavaScript はバージョン1.3相当である。 ( ps:*js-target-version* 変数で確認できる。 ) 変換先が古いからといって、それを ブラウザ上で動作させることには全く問題がない のでいいのだが、一部使いたい機能もあったので頑張って対応させたりしていた。

const キーワード

最近の JavaScript で一番使いたい機能は const なのだが、これは JavaScript 1.3 には無かったものなので、残念ながら Parenscript 2.7 は const を使った JavaScript を生成してくれない。

仕方がないので自分で実装して使えるようにするかと思ったのだが、 Parenscript には生成先の JavaScript にキーワードを簡単に増やす仕組みは(当然ながら)用意されていなかった。 Parenscript の内部シンボルを使いまくることで実装できたが、すぐに壊れそうなものになってしまった。(実装箇所

;;; very hacky 'const' implementation.
(ps::define-statement-operator define-constant (name value &key test documentation)
  (declare (ignore test))
  (let ((value (ps::ps-macroexpand value)))
    `(const ,(ps::ps-macroexpand name)
            ,@(list (ps::compile-expression value) documentation))))

(ps::defprinter const (var-name &optional (value (values) value?) docstring)
  (when docstring (ps::print-comment docstring))
  "const "(ps::psw (ps::symbol-to-js-string var-name))
  (when value? (ps::psw " = ") (ps::print-op-argument 'ps-js:= value)))

class キーワード

もう一つ使いたかったのは class だった。当初の僕が書いた Lisp コードには defstruct が含まれており、これは class に変換して出してしまえばいいかと当初思っていたが、やはり残念ながら Parenscript に class を生成する機能はない。

これをどうしようかと思って少し調べた:

当初、 Paren6 の defclass6 を使う形式で defstruct を実装しようとしたのだが・・結局、元のコードで defstruct を使うのを止めてしまった。

association list を何に対応させるか

Common Lisp連想配列のようなものを使う場合、簡易な場合は dotted list を並べて assocication list を使うことが多いと思う。これをそのまま Parenscript に持ち込もうとしたのだが、残念ながら dotted list を渡した時点で Parenscript はエラーを吐いてしまう。

仕方がないので、 JavaScript のオブジェクトリテラルを吐くように定義した Parenscript マクロを作って誤魔化すことにした。

Common Lisp のオペレータの実装はあまり多くない

当たり前のように使う plusp, first, position などの関数が Parenscript に存在せず、定義する必要があった。 とはいえ、これらの定義は簡単なので大したことではなかった。(実装箇所

(ps:defpsmacro plusp (number)
  `(if (> ,number 0) t nil))

(ps:defpsmacro first (list)
  `(elt ,list 0))

(ps:defpsmacro position (item sequence &key (start 0 start-supplied-p))
  (let ((ret (gensym)))
    `(let ((,ret (ps:chain ,sequence (index-of ,item ,@ (if start-supplied-p `(,start))))))
       (if (= ,ret -1) nil ,ret))))

LOOP マクロの実装は手厚くて便利

Parenscript は loop マクロを使った Lisp コードを変換することができる。 loop マクロのサポートは思っていたより手厚く、 for での destructuring や append にも対応していてかなり便利と思う。 以下のようなグチャグチャのコードでも変換してくれるので便利だった。(引用元

       (loop for (level from unique-weapon) in (find-閃き-alist waza nil)
          for prob = (calc-閃き確率 enemy-waza-level level)
          when (and (plusp prob)
                    (if include-固有技 t (not unique-weapon)))
          collect (list waza prob from level unique-weapon
                        (cond ((includes 装備技-list from)
                               :equipped)
                              ((includes +基本技-list+ from)
                               :basic)
                              ((or (includes dojo-閃き済み技-list from)
                                   (includes +閃き済み技-list+ from))
                               :in-dojo)
                              (t
                               :nowhere))))))

まとめ

Parenscript は現時点でも十分使える。 ただ、最新の JavaScript 文法に対応してないのが残念。今後の対応を期待したい。

変更履歴

  • 2021-03-14 : 公開
  • 2022-08-22 : なぜか gist-it.appspot.com でのコード埋め込みが表示されなくなっていたので、コードを手動で埋め込んだ。