Javaわっかんねーwwwって言ってたらこの開発タームからUIチームに配属されました。最近仕事ではJavaScriptとかJSPを書いてます。
「仕事は楽しいですか」
「んー、、、楽し…くなくはないです」
「JavaScriptは嫌いな言語じゃないですよね」
「まあ、、好きってほどでもないですが」
まあ、Perl5とJavaScriptは嫌いじゃないですが、やっぱりLispに敵うものはありません。そう言うと、
「それなら、Lispで書いてJavaScriptに変換すればいいんじゃないですか」
とかCTOに煽られたので試してみました。
Parenscript概要
Common LispにはParenscriptというライブラリがあります。これはCLでJavaScriptコードを生成するライブラリです。Parenscriptを使えばJavaScriptをS式で記述できます。
Parenscriptは、よくも悪くも、薄いトランスレータです。JavaScriptのコードとParenscriptの記述は一対一で対応しています。
たとえば、以下のJavaScriptコードは、
goog.array.forEach([1, 2, 3], function (x) {
return x % 2;
});
以下のようなParenscriptで書けます。
(ps ((@ goog array for-each) '(1 2 3) (lambda (x) (oddp x))))
関数名の(@ goog array for-each)はJSのシンボルを表しています。
また、コンストラクタはps内でnewを使います。アッパーキャメルケースにするにはハイフンを前置する必要があります。
(ps ((new (@ goog ui -tab-bar)) 'x))
;=> "new goog.ui.TabBar('x');"
ps*はフォームを受け取ってpsで評価する関数です。Parenscriptの中でCLを呼びだそうと思ったらこちらを使います。
冗長すぎる
ただ、これは生のJavaScriptに比べても随分と冗長です。特にシンボルが。
なんで以下のように書けないんでしょう?
(goog.array:for-each '(1 2 3) (lambda (x) (oddp x)))
こう書ければClosure Libraryと同じように記述できるのに!
仕方ない。ラッパー書くか。
Google Closureのラッパーを作る
こういった単純なラッパーのように、呼ばれる関数名によって処理をわけたいとき、特にその関数名が無数にあるときは手作業ではやってはいられません。
こういうときどうするか。AUTOLOADです。こんなこともあろうかと先に書いておきました。
まずは以下のようにundefined-function-hookを定義しておきます。
(defpackage goog.array
(:use :cl :parenscript :cl-ppcre))
(in-package :goog.array)
(setf parenscript:*indent-num-spaces* 2)
(defvar undefined-function-hook
(lambda (condition hook)
(declare (ignore hook))
(let ((symb (cell-error-name condition)))
(lambda (&rest args)
(ps:ps* `((ps:@
,@(mapcar #'intern (cl-ppcre:split "\\." (package-name (symbol-package symb)))) ,symb )
,@(mapcar
(lambda (a)
(if (and (listp a) (eq (car a) 'lambda))
a
(list 'quote a)))
args)))))))
そのあとにgoog.array::for-eachを呼び出すと、
(goog.array::for-each '(1 2 3) '(lambda (x) (oddp x)))
;=>
"goog.array.forEach([1, 2, 3], function (x) {
return x % 2;
});"
といった具合に等価なJSコードを吐いてくれます。エクスポートされたシンボルではないのでコロンが2つ必要なことに注意です。
また、コンストラクタは以下のような関数newを定義しておけば、
(defun new (code) (concatenate 'string "new " code))
これも同じように書けます。
(new (goog.ui::-tab-bar 'x))
;=> "new goog.ui.TabBar('x');"
JSDocとかはParenscriptで吐けそうにないので自前でformatとか用意すればいいかなーって感じです。もしくはdeclareの情報を取ってきて型情報をJSDocに吐くとかできたらクールかもね。
ラッパー全コード
全部で38行です。*goog-packages*にパッケージ名を追加すれば他のパッケージにも対応できます。
(require 'cl-ppcre)
(require 'parenscript)
(in-package :cl-user)
(setf parenscript:*indent-num-spaces* 2)
(setf *debugger-hook*
(lambda (condition hook)
(when (typep condition 'undefined-function)
(let* ((pkg (package-name (symbol-package (cell-error-name condition))))
(local-hook (ignore-errors (symbol-value (intern "UNDEFINED-FUNCTION-HOOK" pkg)))))
(when (and pkg (functionp local-hook))
(setf (symbol-function (cell-error-name condition))
(funcall local-hook condition hook))
(invoke-restart (find-restart 'continue condition)))))))
(defparameter *goog-packages* '(goog goog.ui goog.array))
(defvar *undefined-function-hook*
(lambda (condition hook)
(declare (ignore hook))
(let ((symb (cell-error-name condition)))
(lambda (&rest args)
(ps:ps* `((ps:@
,@(mapcar #'intern (cl-ppcre:split "\\." (package-name (symbol-package symb)))) ,symb)
,@(mapcar
(lambda (a)
(if (and (listp a) (eq (car a) 'lambda))
a
(list 'quote a)))
args)))))))
#.`(progn ,@(loop for pkg in *goog-packages*
collect `(defpackage ,pkg (:use :cl :parenscript :cl-ppcre))))
#.`(progn ,@(loop for pkg in *goog-packages* collect
`(defvar ,(intern "UNDEFINED-FUNCTION-HOOK" pkg)
*undefined-function-hook*)))



最近のコメント