前書き

JavaScriptを書いていて「ここでマクロがあれば…」と思う事案があったので、マクロ欲を満たすためのエントリです。

JavaScriptでのモジュール定義

JavaScriptでcounterモジュールを作ってみます。

var counter = ( function () { var count = 0; var add = function (x) { count += x; return count; } ; return { get: function () { return count; } , increment: function (x) { return add(x); } , decrement: function (x) { return add(-x); } } ; } ()); counter.get(); counter.increment(10); counter.decrement(3); counter.get();

モジュール定義用の構文という訳でもないですが、下記のような基本的な要素の組み合わせでクラスっぽい機能が実現できています。暗記アレルギーな人間としては、こういう考えれば辿れる系のものは覚えやすくて好きです。

とはいえ、何度も書いているとやはり面倒です。ここでマクロがあれば…ということで、Common Lisp（のサブセット）をJavsScriptに変換するライブラリParenscriptを使ってCommon Lispで書きなおしてみます。

Parenscriptで書き直す

Parenscriptで上記のJavaScriptと1対1に対応するコードを書くには少し準備が必要です。

Parenscriptではなぜかhash-tableがサポートされていないので、とりあえず必要なサブセットだけサポートします*1。

( defpsmacro make-hash-table () `( @ {} ) ) ( defpsmacro gethash ( key hash-table ) `( aref , hash-table ,key ) )

まずはこれを直接使って書いてみます。

( defun make-js-module-1 () ( ps ( defvar counter ( funcall ( lambda () ( let* (( count 0 ) ( add ( lambda ( x ) ( incf count x ))) ( public-body ( make-hash-table ))) ( setf ( gethash :get public-body ) ( lambda () count ) ( gethash :increment public-body ) ( lambda ( x ) ( add x )) ( gethash :decrement public-body ) ( lambda ( x ) ( add ( * x -1 )))) public-body ))))))

少し脇道ですが、 make-js-module-1 関数を呼び出すと次のようなJavaScriptコードが得られます。

var counter = ( function () { var count = 0; var add = function (x) { return count += x; } ; var publicBody = { } ; publicBody [ 'get' ] = function () { return count; } ; publicBody [ 'increment' ] = function (x) { return add(x); } ; publicBody [ 'decrement' ] = function (x) { return add(x * -1); } ; return publicBody; } )();

さて、純JSなコードに比べると、 make-js-module-1 では一時変数 public-body を利用していたりと、ハッシュの扱いが不格好です。Common Lispにハッシュの初期化構文に相当するものがないためですが、なければ作ればよいですね。

( defmacro+ps init-hash-table ( &rest pairs ) ( let (( hash ( gensym ))) `( let (( ,hash ( make-hash-table ))) , ( cons ' setf ( mapcan ( lambda ( pair ) `( ( gethash , ( car pair ) ,hash ) , ( cadr pair ) ) ) pairs )) ,hash ) ))

これを使って make-js-module-1 を書き直すと次のようになります。

( defun make-js-module-2 () ( ps ( defvar counter ( funcall ( lambda () ( let* (( count 0 ) ( add ( lambda ( x ) ( incf count x )))) ( init-hash-table ( :get ( lambda () count )) ( :increment ( lambda ( x ) ( add x ))) ( :decrement ( lambda ( x ) ( add ( * x -1 )))))))))))

これで純JSのコードと大体1対1の対応になったので、ようやくスタートラインです。

マクロでイディオムを隠蔽する

まずは、頭の funcall と lambda が鬱陶しいので defmodule マクロで隠してみます。

( defmacro+ps defmodule-3 ( name &body body ) `( defvar ,name ( funcall ( lambda () ,@body )) ) ) ( defun make-js-module-3 () ( ps ( defmodule-3 counter ( let* (( count 0 ) ( add ( lambda ( x ) ( incf count x )))) ( init-hash-table ( :get ( lambda () count )) ( :increment ( lambda ( x ) ( add x ))) ( :decrement ( lambda ( x ) ( add ( * x -1 )))))))))

これだけだと、むしろ分かりにくくなっています。 funcall と lambda が消えたことで、 let* や init-hash-table の意味合いが不明瞭になったためです。

ということで、次の「ルール」を導入することで、この2つを隠します。

モジュール名の次にはプライベートな名前・値のペアをリストで渡す

以降はパブリック名前・値のペアを並べる

( defmacro+ps defmodule-4 ( name private-vars &body body ) `( defvar ,name ( funcall ( lambda () ( let* ,private-vars ( init-hash-table ,@body )))) ) ) ( defun make-js-module-4 () ( ps ( defmodule-4 counter (( count 0 ) ( add ( lambda ( x ) ( incf count x )))) ( :get ( lambda () count )) ( :increment ( lambda ( x ) ( add x ))) ( :decrement ( lambda ( x ) ( add ( * x -1 )))))))

だいぶすっきりしました。

さらに、パブリックな値の定義部分にある lambda を省略します。ただし、定数を直接公開するような使い方ができないという制限がつきます*2。

( defmacro+ps defmodule ( name private-vars &body body ) `( defvar ,name ( funcall ( lambda () ( let* ,private-vars ( init-hash-table ,@ ( mapcar ( lambda ( method-def ) `( , ( car method-def ) ( lambda ,@ ( cdr method-def )) ) ) body ))))) ) ) ( defun make-js-module () ( ps ( defmodule counter (( count 0 ) ( add ( lambda ( x ) ( incf count x )))) ( :get () count ) ( :increment ( x ) ( add x )) ( :decrement ( x ) ( add ( * x -1 ))))))

そんな訳でBefore, Afterです。いくつかの「ルール」や制限*3の導入と引き換えに、モジュール作成に本質的には無関係なキーワードがきれいサッパリなくなりました。

1対1のコードから、たった8行でこれを実現できるLispのマクロは実に強力で気分が良いです。

var counter = ( function () { var count = 0; var add = function (x) { count += x; return count; } ; return { get: function () { return count; } , increment: function (x) { return add(x); } , decrement: function (x) { return add(-x); } } ; } ());

( defmodule counter (( count 0 ) ( add ( lambda ( x ) ( incf count x )))) ( :get () count ) ( :increment ( x ) ( add x )) ( :decrement ( x ) ( add ( * x -1 ))))

マクロの功罪

今回の狭い範囲から見えるマクロの功罪は次のような感じでしょうか。

功 構文を簡単に抽象化できる マクロ名で元になった構文の意図を明確にできる

罪 構文を簡単に抽象化できすぎる 知らなければならないルールが増える 意図的かどうかを問わず、何らかの制限がつく



何か書こうかと思っていましたが、こう並べてみると抽象化一般の功罪と変わらないですね。プログラムの中でもより基盤に近い部分を触るので、影響がより際立つ感じでしょうか。

コード貼り付け

最後に、ここまでを一通りまとめたRoswellスクリプトです。