Parenscript を使ってみた
ちょっとしたおもちゃを作るために Parenscript を使ってみたので忘れないうちに感想を書いておく。
経緯
上のプログラムは、元々は Common Lisp でちょっとした計算を行うものだった。 UI としては REPL をそのまま使っていた。 そのうち、 Lisp 処理系を起動しなくても使いたいとか、スマホでも簡単に見られるようにしたいなどと思いはじめた。そこで、以下の手順で公開しようと思った。
- ここまで書いた Lisp コードを Parenscript で JavaScript に変換する。
- 変換した JavaScript を呼び出す UI を HTML + CSS + JS で作る。
- DOM を触るコードは JavaScript で手書き。
- DOM 操作を Common Lisp で書いて Parenscript で変換して JavaScript を生成することも出来るのだが、そうしなかった。 DOM 操作は SBCL の REPL でデバッグできるようなものではなく、ブラウザの console で扱う方が簡単。そのため、DOM 操作については JavaScript を直接書く方が楽だと判断した。
- 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
を生成する機能はない。
これをどうしようかと思って少し調べた:
- id:eshamster 氏は、以下の記事で
defstruct
を実装している。 https://eshamster.hatenablog.com/entry/2015/11/29/130829 - Paren6 https://github.com/BnMcGn/paren6/ という Lisp ライブラリがあり、これは ES6 文法に対応する
defclass6
などのマクロを提供している。ただ、 Parenscript が JavaScript 1.3 を出力するということは変えず、あくまで Parenscript マクロの範疇での実装になっている。
当初、 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 でのコード埋め込みが表示されなくなっていたので、コードを手動で埋め込んだ。