Parenscriptで少し遊んで見る (3)キャメルケース編

リードマクロによるキャメルケース

今更ですが、Parenscriptでは大文字を表現するために、文字の直前にハイフンを置きます。

CL-USER> (ps (@ document get-element-by-id))
"document.getElementById;"

Common Lispでは通常シンボル名は大文字として解釈される(|Test|のようにすると大文字・小文字を区別可)ための処置で、上記の通り大抵は妥当な見た目になります。

ただ、WebGLでも触ってみようかと、その上に構築されたライブラリであるthree.jsのサンプルをParenscriptに置き換えていたのですが…、「THREE.WebGLRenderer」なるクラス名が出てきました。何かの嫌がらせかと思いましたが、そのまま書き下すと「(@ -t-h-r-e-e -web-g-l-renderer)」とさえないことになります(どちらかと言うと読むより書くのが辛い)。

THREE配下のクラスは他にもあるため、まずは下のようなマクロを書いて「(three -web-g-l-renderer)」のように凌ぎました。

(defmacro+ps three (&rest rest)
  `(@ -t-h-r-e-e ,@rest))

が、どうせなら局所的にキャメルケース(ないしは大文字小文字の区別)を許せないかと考えてみました。シンボル名を大文字として解釈するデフォルトリーダの動作を乗っ取る必要があるので、ここはリーダマクロの出番です。ということで、次のようにして「#j.TEST.WebGLRenderer#」という記述を可能にしてみます。

(eval-when (:compile-toplevel :load-toplevel :execute)
  (set-dispatch-macro-character
   #\# #\j
   (lambda (stream &rest rest)
     (declare (ignore rest))
     (let ((char (read-char stream)))
       (when (or (null char)
                 (not (char= char #\.)))
         (error "\".\" is required in the next of \"#j\"")))
     (let (chars)
       (do ((char (read-char stream) (read-char stream)))
           ((char= char #\#))
         (if (upper-case-p char)
             (progn (push #\- chars)
                    (push char chars))
             (push (char-upcase char) chars)))
       (intern (coerce (nreverse chars) 'string))))))

readを使っても大文字になったシンボルが返ってくるだけなので、read-charで一文字ずつ取り出して、大文字の場合は直前にハイフンを入れつつcharsにpushしていきます。これで、「#j.TEST.WebGLRenderer#」は「-t-h-r-e-e.-web-g-l-renderer」に変換され、さらに前回の"ps."マクロ内でこれを使うことで無事「(@ -t-h-r-e-e -web-g-l-renderer)」が得られます。

なお、キーワードに選んだ"j"はjavascriptの"j"です。また、最初は「#jTEST.WebGLRenderer#」("j"の後の"."なし)としても、大抵は次が大文字なので大丈夫じゃないか…と思ったのですが、意外と見にくかったので"."を必須としました。

試しに書いてみる

ここまでの3回で書いたマクロを使ってサンプルを書いてみます。なお、defun.psは第一回のdefun+psに第二回相当のドット記法を追加したものです。

対象とするのは「初心者でも絶対わかる、WebGLプログラミング<three.js最初の一歩> | HTML5Experts.jp」で紹介されているthree.jsのサンプル+αです。まず直接書いたものが以下です。

(defun.ps rotate-mesh (mesh)
  (with-slots ((rot rotation) (pos position)) mesh
    (rot.set 0
             (+ rot.y 0.01)
             (+ rot.z 0.01))
    (if is-keydown
        (pos.set 0 0 0)
        (pos.set (+ pos.x 0.05)
                 (+ pos.y 0.05)
                 0))))

(defun.ps main ()
  (let* ((scene (new (#j.THREE.Scene#)))
         (width 600)
         (height 400)
         (fov 60)
         (aspect (/ width height))
         (near 1)
         (far 1000)
         (camera (new (#j.THREE.PerspectiveCamera# fov aspect near far)))
         (renderer (new #j.THREE.WebGLRenderer#)))
    (camera.position.set 0 0 50)
    (renderer.set-size width height)
    (document.body.append-child renderer.dom-element)
    (let ((light (new (#j.THREE.DirectionalLight# 0xffffff))))
      (light.position.set 0 0.7 0.7)
      (scene.add light))
    (let* ((geometry (new (#j.THREE.CubeGeometry# 30 30 30)))
           (material (new (#j.THREE.MeshPhongMaterial# (create :color 0xff0000))))
           (mesh (new (#j.THREE.Mesh# geometry material))))
      (scene.add mesh)
      (labels ((render-loop ()
                 (request-animation-frame render-loop)
                 (rotate-mesh mesh)
                 (renderer.render scene camera)))
        (render-loop)))))

defun.psをそれぞれexpandして、ほぼ素のps相当であるdefun+psにしてみると、以下のようになります(見栄えを揃えるためにmacroexpandの結果を整形しています)。

(defun+ps rotate-mesh (mesh)
  (with-slots ((rot rotation) (pos position)) mesh
    ((@ rot set) 0
                 (+ (@ rot y) 0.01)
                 (+ (@ rot z) 0.01))
    (if is-keydown
        ((@ pos set) 0 0 0)
        ((@ pos set) (+ (@ pos x) 0.05)
                     (+ (@ pos y) 0.05)
                     0))))

(defun+ps main ()
  (let* ((scene (new ((@ -t-h-r-e-e -scene))))
         (width 600)
         (height 400)
         (fov 60)
         (aspect (/ width height))
         (near 1)
         (far 1000)
         (camera (new ((@ -t-h-r-e-e -perspective-camera) fov aspect near far)))
         (renderer (new (@ -t-h-r-e-e -web-g-l-renderer))))
    ((@ camera position set) 0 0 50)
    ((@ renderer set-size) width height)
    ((@ document body append-child) (@ renderer dom-element))
    (let ((light (new ((@ -t-h-r-e-e -directional-light) 0xffffff))))
      ((@ light position set) 0 0.7 0.7)
      ((@ scene add) light))
    (let* ((geometry (new ((@ -t-h-r-e-e -cube-geometry) 30 30 30)))
           (material (new ((@ -t-h-r-e-e -mesh-phong-material) (create :color 0xff0000))))
           (mesh (new ((@ -t-h-r-e-e -mesh) geometry material))))
      ((@ scene add) mesh)
      (labels
          ((render-loop ()
             (request-animation-frame render-loop)
             (rotate-mesh mesh)
             ((@ renderer render) scene camera)))
        (render-loop)))))

コードを劇的に短くするような改良ではないので微妙な差かもしれません。書いている分にはだいぶ書きやすいのですが。ただ、rotate-mesh関数については"@"が見た目の上でも非常に鬱陶しく、中身がすっと頭に入ってこないと感じますがどうでしょうか。

最後に、これを以下のような関数でjavascriptに出力します。なお、js-main内で使っているwith-use-ps-packは、第一回のwith-import-ps-defの改良(と信じている)版です。この辺りの話はまた次回に。

(defun js-main ()
  (with-use-ps-pack (this)
    (defvar is-keydown false)
    (window.add-event-listener "keydown" (lambda (e) (setf is-keydown true)))
    (window.add-event-listener "keyup" (lambda (e) (setf is-keydown false)))
    (window.add-event-listener "DOMContentLoaded" main false)))

結果は以下のとおりです。

function rotateMesh(mesh) {
    mesh.rotation.set(0, mesh.rotation.y + 0.01, mesh.rotation.z + 0.01);
    return isKeydown ? mesh.position.set(0, 0, 0) : mesh.position.set(mesh.position.x + 0.05, mesh.position.y + 0.05, 0);
};
function main() {
    var scene = new THREE.Scene();
    var width = 600;
    var height = 400;
    var fov = 60;
    var aspect = width / height;
    var near = 1;
    var far = 1000;
    var camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
    var renderer = new THREE.WebGLRenderer;
    camera.position.set(0, 0, 50);
    renderer.setSize(width, height);
    document.body.appendChild(renderer.domElement);
    var light = new THREE.DirectionalLight(0xffffff);
    light.position.set(0, 0.7, 0.7);
    scene.add(light);
    var geometry = new THREE.CubeGeometry(30, 30, 30);
    var material = new THREE.MeshPhongMaterial({ 'color' : 0xff0000 });
    var mesh = new THREE.Mesh(geometry, material);
    scene.add(mesh);
    var renderLoop = function () {
        requestAnimationFrame(renderLoop);
        rotateMesh(mesh);
        return renderer.render(scene, camera);
    };
    return renderLoop();
};
var isKeydown = false;
window.addEventListener('keydown', function (e) {
    return isKeydown = true;
});
window.addEventListener('keyup', function (e) {
    return isKeydown = false;
});
window.addEventListener('DOMContentLoaded', main, false);

今回はほぼ1対1対応なので、JavaScriptに対する優位性は余りないかと思います。ただし、今回の範囲でもParenscriptのデフォルトマクロであるwith-slots(rotate-mesh関数内)は優位性を主張できる部分かと思います。同様にマクロを利用することで記述量を大幅に減らせる可能性があるというのがParenscriptの優位性と言えるでしょうか。

Lispそのものと同じで、本当にそうなのかは実際にもっと書いてみないと分からなさそうですが。

動作可能なサンプル

一応動作可能なサンプル(caveman2上で構築)は以下です。試す人もいないと思うのでおざなり解説ですが、quicklisp管理下にclone(submoduleのinit, updateも必要)後、(ql:quickload :caveman-sample)をし、(caveman-sample:start :port 8080)のようにすれば指定のポートで動作します。念のため、今回の記事時点でつけたタグは"blog-play-ps-3"です。

github.com

Parenscript関連記事

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