JavaScriptのモジュール定義構文をParenscriptで抽象化(マクロで遊ぶ)
前書き
JavaScriptを書いていて「ここでマクロがあれば…」と思う事案があったので、マクロ欲を満たすためのエントリです。
JavaScriptでのモジュール定義
JavaScriptでcounterモジュールを作ってみます。
var counter = (function() { var count = 0; var add = function(x) { // こんな感じでprivateな関数も書けます、というためだけに用意した関数 count += x; return count; }; return { get: function() { return count; }, increment: function(x) { return add(x); }, decrement: function(x) { return add(-x); } }; }()); // 使い方 counter.get(); // -> 0 counter.increment(10); // -> 10 counter.decrement(3); // -> 7 counter.get(); // -> 7
モジュール定義用の構文という訳でもないですが、下記のような基本的な要素の組み合わせでクラスっぽい機能が実現できています。暗記アレルギーな人間としては、こういう考えれば辿れる系のものは覚えやすくて好きです。
- クロージャ
- ハッシュの要素にはドットでアクセス可能
- 変数を通した関数アクセスに(Common Lispのfuncallのような)特別な処理は不要
とはいえ、何度も書いているとやはり面倒です。ここでマクロがあれば…ということで、Common Lisp(のサブセット)をJavsScriptに変換するライブラリParenscriptを使ってCommon Lispで書きなおしてみます。
Parenscriptで書き直す
準備:JavaScriptと(ほぼ)1対1の書き方へ
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
マクロで隠してみます。
;; make-js-moduleと番号を合わせるため1, 2は欠番 (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のマクロは実に強力で気分が良いです。
// Before 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); } }; }());
;; After (defmodule counter ((count 0) (add (lambda (x) (incf count x)))) (:get () count) (:increment (x) (add x)) (:decrement (x) (add (* x -1))))
マクロの功罪
今回の狭い範囲から見えるマクロの功罪は次のような感じでしょうか。
- 功
- 構文を簡単に抽象化できる
- マクロ名で元になった構文の意図を明確にできる
- 罪
- 構文を簡単に抽象化できすぎる
- 知らなければならないルールが増える
- 意図的かどうかを問わず、何らかの制限がつく
何か書こうかと思っていましたが、こう並べてみると抽象化一般の功罪と変わらないですね。プログラムの中でもより基盤に近い部分を触るので、影響がより際立つ感じでしょうか。
コード貼り付け
最後に、ここまでを一通りまとめたRoswellスクリプトです。
[JavaScript] ブラウザからSuperAgentでファイルをPOST
ブラウザからSuperAgentでファイルをPOSTしようとしてハマったのでメモ。なお、SuperAgentはAJAX通信に特化した軽量なJavaScriptライブラリです。
SuperAgent紹介記事リンク:jQuery.ajaxの代わりにSuperAgentを使う - Qiita
ハマった部分
まず、HTMLでformのsubmitを使ってファイルを送る場合は次のようになります。enctype="multipart/form-data"
が唯一ポイントで、後はtype=fileなinputを利用するだけです。
<form name="main_form" action="/some-url" enctype="multipart/form-data" method="POST"> <input name="submit_file" type="file"> <input type="submit" value="ファイル送信"> </form>
もう少し柔軟に制御したかったのでSuperAgentで自分でAJAX通信をしようと思いました。そこでドキュメントを見ると、ファイルを送るには.attach(name, [path], [filename])
関数を使うように書いてあります。
// SuperAgentのドキュメントから引用 request .post('/upload') .field('user[name]', 'Tobi') .field('user[email]', 'tobi@learnboost.com') .attach('image', 'path/to/tobi.png') .end(callback);
第2引数のpath
って何さと思い、フルパスを渡してみたりと色々的はずれな試行錯誤をしていました。しかし、どうやらこの記述はNode.jsすなわちサーバ側で利用する場合の説明であって、ブラウザ側ではまた違うものを渡す必要があるようです。
そしてそのブラウザ側での話は書かれていない(はず)…。
結論
結局どうするかですが、Fileオブジェクトを渡せば良いようです。冒頭のformを例に取ると、Fileオブジェクトは下記のように取り出せます。
document.main_form.submit_file.files[0]
実際、SuperAgentのソースで.attach()
関数のコメントを見るとFileオブジェクトかBlobオブジェクトを渡せと書いてあります。
引用元:https://cdnjs.cloudflare.com/ajax/libs/superagent/1.2.0/superagent.js
/** * Queue the given `file` as an attachment to the specified `field`, * with optional `filename`. * * ``` js * request.post('/upload') * .attach(new Blob(['<a id="a"><b id="b">hey!</b></a>'], { type: "text/html"})) * .end(callback); * ``` * * @param {String} field * @param {Blob|File} file * @param {String} filename * @return {Request} for chaining * @api public */
…なんとなく尻切れ感がありますが、他に書きたいこともないので終わり。
[Common Lisp] システム内のパッケージ間の関係をグラフ化
システム内に存在するパッケージ間の参照関係をgraphvizでグラフ化するRoswellスクリプトmake-package-tree.ros
を書いてみました。リファクタリングに使える…かもしれません。
前説
これを作ったきっかけの話です。
Common Lispを始めた頃に、とりあえず練習用でオセロのプログラムを書いていました。単純なミニマックス探索(αβ法)と単純なモンテカルロ木探索(UCT)*1を備えていて、簡単なCUIインタフェースもつけてます。
そうして右も左も分からない中で書いたのがothello-clです*2。パッケージを分けたり、SBCL, CCLの両対応にしたりと、これをもう少し整えたものが下のcl-othelloです。
このとき、とりあえずテストを通すこと優先で各パッケージでひたすらexportしていたのですが、だいぶ余計なものをexportしている気がしました。そんなわけで、リファクタリングついでにグラフ化して見てみよう、というのが今回のスクリプトを作った動機です。
使用感
使い方
引数なしでヘルプが見れます。こうしたヘルプの生成兼コマンドライン引数の処理にはCL-CLIを利用しています。
$ ./make-package-tree.ros ./make-package-tree.ros [OPTION]... SYSTEM-NAME [ OPTIONS ] Global options: -P,--only-package Show only packages (doesn't show symbols) -o,--output <file> Place the output into <file> (default: temp.png) -e,--exclude <package names> Exclude packages from graph (if you exclude multiple packages, write them separating by space)
- システム名には
(ql:quickload ...)
でロードできるシステムの名前(小文字可)を入れます - アウトプット名は特に解析していないので、拡張子に関わらずPNGしか出ません
- excludeの複数指定は"cl-othello cl-othello.utils"のような感じです
次に適用結果の一部を拡大して見方を説明します。
- 四角いボックスはパッケージ
- 内部の楕円はエクスポートしているシンボル
- 入ってくる矢印は他パッケージからの参照
- ただし、
use-pacakge
されている場合は省略
- ただし、
- 入ってくる矢印は他パッケージからの参照
- 小さい円はパッケージ自身*3
- 入ってくる矢印は
use-package
されていることを示す(上の例にはないですが…) - 出て行く矢印はシンボルを
import
もしくはパッケージをuse-package
していることを示す
- 入ってくる矢印は
- 内部の楕円はエクスポートしているシンボル
cl-othelloに適用した結果
Before: おおむねothello-clからの移植が終わった時点。コミットID:ba72a26...
After: 記事時点のコミット。コミットID:eb3fe18...("CL-OTHELLO"パッケージをexclude)
なるほど、分からん。
まあパッと見をどうこうするというよりは、ざっと眺めていって、参照されてないシンボルがあるけどこれexport必要だっけ、とか、このパッケージのシンボル1個だけ参照してるけど不要な参照してないっけ、とかを考えるためのものです。
ちなみに、--only-package
をつけるとこんな感じの出力になります。汚い…。
実装について
対象となるパッケージを取り出す
まずは、システム内のパッケージを一通り取り出す部分です。怪しげなので、もっと賢い方法があれば知りたいです…。
;; ロードメッセージ省略コードやreadtableを戻すコードは省略 (defvar *unprocessed-pack-list* nil) (defun make-macroexpand-hook-fun (old-hook) (lambda (fun form env) (when (and (consp form) (eq (car form) 'cl:defpackage)) (pushnew (cadr form) *unprocessed-pack-list* :test #'string=)) (funcall old-hook fun form env))) (defun load-target-package-list (system-name) (ql:quickload system-name) (let ((*macroexpand-hook* (make-macroexpand-hook-fun *macroexpand-hook*))) (asdf:load-system (intern system-name "KEYWORD") :force t)))
ここはql:quickload
のパッケージ名出力コードを参考にしています。*macroexpand-hook*
にフックをかけて、defpackage
が来たらそのパッケージ名をリストに入れるというのが基本的な考え方です。しかし、ql:quickload
では依存パッケージが全て読まれてしまうため、どれが対象のsystem下のパッケージか分からないという問題があります*4。そこで次のような力業に出ています。
- とりあえず
ql:quickload
で全てロード - 上記のフックをかける
(asdf:load-system ... :force t)
で対象システムを無理やり読み直す- 依存システムはロード済みなので、対象システム下のパッケージだけがとれる…はず
参照している他パッケージのシンボルを取り出す
残りは末尾に全コードを貼り付けたので、書きたいところだけ簡潔に…。
どのパッケージのシンボルをインポートしているかを調べる処理のメインはinterpret-package
関数です。do-symbols
で各パッケージ内の全シンボルを調べて、exportされているシンボルと、同システム内の別パッケージから継承したシンボルを記録しています。これで漏れが出ない…はず。
また、パッケージ間の関係を木構造(正確には有向グラフ?)と見た場合、幅優先探索の順序でパッケージを見ています(起点は上記のパッケージ探しで最初に見つけたもの)。この方がgraphviz上で元の木構造に近い形が得られやすいためです。実際、深さ優先探索版をcl-othelloに適用したところ、ほぼ全パッケージが縦に並んでしまい、うまく配置できないようでした。ただ、こうした探索自体が歴史的経緯*5で必要だったもので、今なら単に上記で見つけた順で問題ない気もします。
グラフ化
graphviz用のコードを出すためにs-dotというライブラリを利用しています。graphvizのドット形式をS式で書くためのDSL兼レンダラです。
ただ困ったことに、DSLのキーワードであるnode
やらedge
やらが全てs-dot
パッケージのシンボルとeq
判定をとっていて、しかもexportされていません…*6。一個や二個ではないので、対症療法としても一々s-dot::node
のように書くのも面倒です。そんなわけで、下記のリードマクロで$node
のように書けるようにしています。
(set-macro-character #\$ #'(lambda (stream &rest rest) (declare (ignore rest)) (let ((sym (read stream nil))) (intern (symbol-name sym) "S-DOT"))))
全コード
この記事時点のコードを貼り付けます。
感想
こうメタな情報に普通にアクセスできるのはなんだか気分がいいですね。
*1:モンテカルロ木探索といえばAlpha Goが話題ですね。Deep Learningばかり話題になっている感もありますが、2004年に登場したモンテカルロ木探索というブレイクスルーあってのものだとこっそり主張しておきたかったりします。個人的には、モンテカルロ木探索がDeep Learningという翼を得てさらに飛翔するのか、翼だけ飛んでいってしまうのか気になってます。
*2:「clなんとか」という名前順になっていない辺り分かってなかった感が目に見えます
*3:本当はボックス(cluster)から直接線を伸ばしたいのですが、それができないので代替手段です
*4:quickloadの方は依存パッケージも全て出力するのでこの問題は関係ありません
*5:元々system内のパッケージ一覧を取り出す方法を思いつかなかったので、システム名と同名のパッケージがあると仮定(ないとエラー)して、そこから辿っていました
*6:まあ大量にexportされても困るのでキーワードにしておいて欲しかったという話です。実際、キーワード利用に変更したs-dot2というプロジェクトがあったりします(quicklispのリポジトリには登録されていませんが…)
Node.js本のサンプルをCommon Lispで書く
JS+Node.jsによるWebクローラー/ネットエージェント開発テクニック
- 作者: クジラ飛行机
- 出版社/メーカー: ソシム
- 発売日: 2015/08/31
- メディア: 単行本
- この商品を含むブログ (2件) を見る
こんな本を買いました。タイトルを見てもピンと来なかったのですが、目次やレビューを見ると新し目の技術を広く浅く見るのに良さそうと感じた次第です。
当然サンプルコードはJavaScriptで書かれているわけですが、そのまま書き写しても面白くないので、前回記事で作った環境上を使ってCommon Lispで書いてみることにしました。Parenscriptを利用して、Common Lispで書いたコードをJavaScriptコードに変換する方針です。
結果としては、次の手順で作れるようになりました。単一ファイルのスクリプトのみ対応済みです。
init.ros
(Roswellスクリプト)でテンプレートからlispファイルを作成- プログラム本体や(Node.jsの)依存ライブラリを記述
run.ros
でJavaScriptへ変換&実行
テンプレート:skelton/skelton.lisp
次のようなテンプレートを用意しました。
<% @var name %>
などと書いているのはcl-emb(紹介記事「HTML内にコードを埋め込めるテンプレートエンジン「CL-EMB」 - Qiita」)によるコード埋め込み書式です。
大体次のような感じで使います。
- npm installしたい依存ライブラリは
*dependencies*
に書く(例.'("cheerio-httpcli" "request")
) - casperjsコマンドなど、nodeコマンド以外から実行したい場合は、
*executor*
を書き換える - メインの処理は
with-use-ps-pack
内に、その他関数などは特定の書式でトップレベルに書く
一行目でしれっとps-experiment
なるものをquickload
していますが、自作のParenscript拡張です。quicklispリポジトリには入っていないので、適切なフォルダ下(前回記事の手順であれば~/.roswell/local-projects/
下)にgit cloneしておく必要があります。
どんな感じに使えるのかは記事後半のサンプル参照。
テンプレートからlispファイルを生成:init.ros
上記のテンプレートからlispファイルを生成します。
引数処理が長々しいですが、処理としてはcl-embによる変換を噛ませている程度で、ほぼテンプレートのコピーをしているだけです。次のような感じで使います。この例であれば、part02/download-node.lisp
を編集して必要な処理を書いていきます。
$ ./init.ros <ディレクトリ名> <プログラム名> # $ (例)./init.ros part02 download-node # Create part02/download-node.lisp
一応の事故防止処置として、存在しないディレクトリを指定した場合や、既存のファイル名と被る場合はエラーにしています。
lispファイルをjsファイルに変換して実行:run.ros
編集が終わったら実行です。
長々と処理していますが、やりたいことはmain
関数の最後の数行です。
init.ros
で生成 & 手で編集したlispファイルをロードし(load
)- JavaScriptファイルをWORKディレクトリ下に書き出し(
output-js
) - 未インストールの依存npmパッケージをインストールし(
install-by-pm
) - node.jsスクリプトとして実行します(
run-shell
)
3のインストールチェックですが、直下のnode_modules
フォルダに該当する名前のフォルダがあるかどうかだけを見る超簡易判定です。npm install
自体にインストールスキップ機能がないか調べましたが、見つからなかったのでこんな感じにしました。
package.jsonにリポジトリ内で使うライブラリを全てまとめる…というのが普通のやり方のような気がしますが、たかがスクリプトなのでql:quickload
のように気軽に書きたかった感じです。
使用例
少し長めですが、関数作成とnpmモジュールの利用の2つが揃ったものとしては一番短い例を載せます。Wikipedia日本語版のハムスター*1の記事から画像をダウンロードするコードです。
ここで、ps-experiment
の目立った機能としてはdef~.ps
系マクロとwith-use-pack
マクロがあります。def~.ps
系マクロはトップレベルでParenscript用の関数やら変数やら(あと構造体)を定義するためのものです*2。
これらの定義を含めてJavaScriptコードを吐き出すのがwith-use-pack
の役割です。第一引数のリストで定義を読み込むパッケージを指定します(:this
は自パッケージのエイリアス)。スクリプトレベルだと関係ありませんが、use-package
の関係にあるパッケージは依存関係と見て再帰的に定義を読み込みます*3。なお、パッケージマネージャもどきなので名前空間を分けてくれたりはしません。
他細かい点。
--
はps:chain
の別名です。安易にこういう別名を提供するのは好きではないですが、chain
が余りにも良く出てくるので堪え切れませんでした…*4。インデントにもろに効くのに5文字は長い- ドット記法は内部的に@記法?に変換される("a.b.c" → "(ps:@ a b c)")ので安心です。JavaScriptへの変換時にドット記法に戻るため一見無駄に見えますが、
ps:with-slots
などとの兼ね合いで必要です - URLエンコードが少々曲者です。マルチバイト文字列はParenscriptがユニコードエスケープ形式に変換する(してしまう)ので、先にURLエンコードしています(
url-encode
マクロ)
最後に、生成されるコードは以下のようになります。生成後の方が短いとかいうのは野暮です。こういったスクリプトレベルでは、Lispで書けるという以上の利点は薄いかもしれません。
var CLIENT = require('cheerio-httpcli'); var REQUEST = require('request'); var FS = require('fs'); var URL = require('url'); var SAVEDIR = __dirname + '/img'; function ensureSavedir() { return !FS.existsSync(SAVEDIR) ? FS.mkdirSync(SAVEDIR) : null; }; function downloadImages() { var url = 'https://ja.wikipedia.org/wiki/' + '%E3%83%8F%E3%83%A0%E3%82%B9%E3%82%BF%E3%83%BC'; var param = { }; return CLIENT.fetch(url, param, function (err, $, res) { if (err) { console.log('error'); console.log(err); return null; }; return $('img').each(function (idx) { var src = URL.resolve(url, $(this).attr('src')); var fileName = URL.parse(src).pathname; var filePath = SAVEDIR + '/' + fileName.replace(/[^a-zA-Z0-9\.]+/g, '_'); return REQUEST(src).pipe(FS.createWriteStream(filePath)); }); }); }; ensureSavedir(); downloadImages();
こういったものは、Clojure Script擁するClojureの方が良いのかもしれないですね。そちらも調べておきたいです。
*1:本ではネコだった気がしますが気のせいです
*2:ちなみに、「def~.ps+」系のマクロを利用すると、Parenscript用定義とCommon Lisp用定義を同時にできます
*3:今は循環参照のチェックをサボっているので、循環参照させると無限ループします… → 2016/02/16 修正済み
*4:ハイフン2つでchain=鎖のイメージです。ClojureのJava関数のチェインで".."が使われているのでなぞるべきだと思ったのですが、Common Lispではエスケープが必要になるので断念…。
Common Lisp開発環境を新規に作ったのでメモ
JavaScriptの本「JS+Node.jsによるWebクローラー/ネットエージェント開発テクニック」を買ったのでCommon Lispの環境を新規に作ってみました(正しい日本語です)。
Emacsには抵抗がなく、これからCommon Lispを始めたいという人にもちょうど良いぐらいの内容ではないかと思います。ということで、そんな体で書いていきます。
以下の状況からスタートします。基本的には本書の「第1章03 環境を構築しよう」が完了した時点と思えば良いです。
- VirtualBoxとvagrantをインストール
- phphpet/centos65-x64から仮想マシンファイルを落としてCentOS 6.7*1環境を作成
- 仮想マシン内で、sudo yum install gitでgitをインストール
CL開発環境の構築は基本「Modern Common Lisp」を参照すれば良いと思っていますが、主な差分は以下の点です。
- 処理系はRoswellを通して入れましょう
- 色々楽になる
- quicklispのインストールも不要になる
- Emacsは24以降を入れましょう
- 上記で紹介されているあれこれのEmacs拡張が簡単にインストールできる
ここではほぼ入れ方しか書いていないので、使い方は同リンクを参考にすると良いです。
Emacs24.5のインストール
Emacs 24以降は標準でパッケージマネージャ(init.elからはpackage-install
で利用可)がついているのでこれをインストールします(yumで入るのはもう少し古いはずです)。下記は執筆時点で最新の24.5の例です。
sshからの利用を前提にしているので、--without-x
を入れている辺りがポイントでしょうか。
$ sudo yum install lcurses-devel $ wget http://mirror.jre655.com/GNU/emacs/emacs-24.5.tar.gz $ tar zxvf emacs-24.5.tar.gz $ cd emacs-24.5 $ ./configure --without-x $ make $ sudo make install
Roswellをインストール
※この先はおおむねスクリプト化しています(記事末尾に貼り付け)。Emacsだけスクリプトに入っていないのは単にそういう手順で入れてしまったからで、特に意味はありません。
少しだけRoswellの説明
Common LispではSBCLやらClozure CLやら処理系が色々あったり、意外とインストールに失敗したりと入り口でつまづきがちです。
今ならとりあえずRoswell入れておけばこうした煩わしさから解放されます。このRoswell自体は処理系ではありません。入れておくとシェル上からros install sbcl-bin
やros install ccl-bin
*2とコマンドを一発叩くだけで主要な処理系をインストールすることができるようになります。しかも、Common Lispにおけるパッケージ管理のデファクトであるquicklispレディな状態でインストールされるため、quicklispのインストール手順も省略できます。
Roswellは処理系インストールだけが機能ではないので、深町さんによる紹介記事「Common Lispとリアル・ワールドを繋ぐ「Roswell」の紹介 - 八発白中」等も参考にすると良いです。
インストール
基本的には普通のconfigure, make, make installですが、先に必要なものを入れておきます。
まずyumで色々入れます。この先で必要なものもまとめてしまったので、どれがRoswell入れる上で最低限だったか覚えていないです…(libcurlぐらいだったような…)。
$ sudo yum -y install libcurl-devel zlib-devel perl-ExtUtils-MakeMaker
CentOS 6系では大抵autoconfのバージョンが古くてconfigureに失敗するので、2.65未満の場合は新しいバージョンを入れておきます。
$ wget http://ftp.gnu.org/gnu/autoconf/autoconf-2.69.tar.gz $ tar zxf autoconf-2.69.tar.gz $ cd autoconf-2.69 $ ./configure $ make $ sudo make install
いよいよRoswellを入れます。
$ git clone -b release https://github.com/roswell/roswell.git $ cd roswell $ sh bootstrap $ ./configure $ make $ sudo make install
ros run
と打ってみると(初回のみ)SBCLがインストールされた後にREPLが起動します(下記はインストールの出力は略)。
$ ros run * (+ 1 2) 3 * (quit)
オプション
ここで説明するものは、Emacs + Common Lispな環境をセットするだけであればオプションですが、下記に貼り付けた.emacs.d/init.el
では前提になっています。概要のみ記述しますので、インストール方法の詳細は末尾のスクリプトを参照してください。
より新しいバージョンのGitを入れる
EmacsからGitを操作できるmagitという拡張がありますが、(執筆時点で)バージョン1.9.4以降のgitを求められます。yumで入るものは1.7系とやたら古いので新しいものを入れておきます。
ちなみに、個人的にmagitで特に重宝しているのは、git branch
などリポジトリのファイルを書き換える操作を行った時に、バッファ内のファイルをアップデートしてくれるという点です。基本的に開いたバッファは開きっぱなしなので大変助かってます。また、いったんM-x magit-status
で開いてしまえば、主要な操作方法はhキーを押せばいつでも見られるという点も助かる点です。
HyperSpecをローカルに入れておく
HyperSpecはCommon Lispの仕様書…ではありませんが、事実上仕様書のようなものです。頻繁に利用することになるので、Emacsからローカルに参照できるようにしておくと便利です。
slime-repl-ansi-color.elを入れる
SLIME(参考:モダンCommon Lisp第3回: SLIMEの使い方 基礎編 | ありえるえりあ)で色の表示を可能にする拡張です。
Emacsの設定と準備用スクリプト
新規環境であれば、Emacsを入れたら後は下記のスクリプトを走らせて、.emacs.d/init.elに下記の内容をコピーするだけでも動くはずです。
あくまで自身のメモを兼ねているので、最小限の環境ではないです。が、お勧め設定+αぐらいに割りと収まっていると思います、きっと…。
prepare.sh
上記で書いてきたEmacs以外のインストールをまとめたスクリプトです。注意ですが、平気でprefixなしのsudo make installをしているので既存環境でいきなり走らせることはお勧めしません。
何点か補足。
- L37: gitのバージョンチェックが超雑です
- L70: SBCLのインストールも済ませてしまおうと、一回Roswellを起動・終了しています
~/.emacs.d/init.el
余り害のあるものは入れていないつもりです。ただし、慣れないうちはparedit関係のものはコメントアウトしておくと良いと思います。;; paredit
(108行目)とコメントがある少し下のadd-hook
3つをコメントアウトしておけば特に害はないはずです。なお、気になってきたら「ParEdit チュートリアル」辺りを参考に試してみると良いと思います。慣れれば非常に快適です。
Common Lispに直接関係のあるものは81行目(;; ----- Lisp ----- ;;
とコメントのある行)以降です。
何点か補足。
認証付きプロキシ環境でも(多少は)快適なpackage-installの利用
Emacs24を入れてから、list-packages
で適当にパッケージを追加してきたのですが、そろそろ.emacs.d/init.el
のポータビリティが怪しくなってきたので、環境を見直し始めました。当初は、CaskやEl-getといったモダンな?パッケージマネージャを使おうと意気込んでいたのですが、認証付きプロキシ環境でどうもうまく動きませんでした…。プロキシが原因なのかはよく分かりませんが。
今後も振り回されるかも知れないと思うと、解決は目指さずに(手元の環境下で)実績のあるデフォルトのpackage-install
に頼る方向にしました。
結果、「結局 package.el に戻ってきた / マスタカの ChangeLog メモ」を参考に以下のようにしました。package-refresh-contents
の呼び出しを、新しいパッケージのインストール時に限定している辺りが唯一のポイントです。呼び出すとプロキシのユーザ名・パスワードを求められるため、むき出しにするとEmacsを起動するたびに死にたくなります。かといって呼ばないとインストールできないためこうなりました。
ユーザ・パスワードの入力が新しいパッケージをインストールするときに限定されるので、「多少は」マシかと思います。
(require 'package) (add-to-list 'package-archives '("melpa" . "http://melpa.milkbox.net/packages/") t) (package-initialize) (defun install-packages (packages) (let ((refreshed nil)) (dolist (pack packages) (unless (package-installed-p pack) (unless refreshed (package-refresh-contents) (setq refreshed t)) (package-install pack))))) (install-packages '(auto-complete magit markdown-mode))
まあsocatなどでローカルにプロキシサーバを立てて認証を代理させるのが筋な気はしますが…。書いてしまったものは仕方がない!
ちなみに、bashの環境変数に設定しているhttp_proxy
やhttps_proxy
のプロキシのアドレスは認識してくれたのですが、一緒に書いた認証情報は無視されるようでした。
2015年に作っていたもの
2015年に作ったものまとめ。1つ例外を除いて全部Common Lispでした。
全体的に飽き性っぷりが垣間見えるだけな気もしますが、来年どれかは発展させていきたいです。
オセロ
(旧版:sample-of-eshamster/othello-cl · GitHub)
作るものに困ったらとりあえずオセロを作れば良いんじゃないか、と思って作りました。学習機構は作ってないですが、簡単な静的評価関数+αβ探索とUCT(Upper Confidence bounds applied to Trees)ぐらいは作りました。あとは簡単なCUIインタフェースつきです。
適当に作った結果、ゲーム開始地点から終局までのランダムシミュレーションが(初期局面へのリバース込みで)秒間30回というひどい値になっていたので、しばらく最適化して秒間200回ぐらいにしました。さらに、この年末にも最適化して遊んでいたところ、秒間1000回ぐらいになりました。相場が分かりませんが、実用にはまだ桁が足りない気がします*1。
備忘録も兼ねて最適化周りの話はそのうち記事にしておきたいです。
ちなみに、旧版は右も左も分からない頃にClozure CL上で書いていたもので、新版はSBCLにも対応しつつパッケージ周りを少し整えて移植したものです。
cl-lazy
Land of Lispにはマクロによる遅延評価の実装が出てきます。面白そうだったので自分でも書いて色々遊んでみたのがこのライブラリです。
遅延評価と言えば無限長数列というイメージがあったので、とりあえずフィボナッチ数列を作ってみました。以外と手間取ったので、まずはマクロで苦労した部分を覆い隠してみました。それでも、数学的な記法による定義(漸化式)と比べると分かりにくいのでリードマクロで数列定義文法を作ってみました。
そんな感じで、思いの他Common Lispのマクロ方面を色々触ることができて楽しかったです。
結果ですが、#<fib[n] = 0, 1, (+ fib[n-2] fib[n-1])>
で無限長フィボナッチ数列が定義できるようになりました。ワンライナーで数列を定義できる言語は数あれど、ここまで分かりやすいものはないんじゃないか、と密かに思ってます。
遅延評価部分よりも数列処理部分を取り出して発展させる道はありかもと思いつつ、自分で利用する場面がないので放置してます。
cl-prime-number
cl-lazyの遊びの一貫です。無限長素数列を作って遊んでいます。具体的には、素因数分解や、分解後の素数指数表現の中で最小公倍数や最大公約数を求める関数を作っています。
ちなみに、Gaucheの持つ素数ライブラリは恐ろしく速いですね。例に出ている素因数分解をcl-prime-numberに投げても、結果が返るよりもCtrl+Cを押す誘惑に負ける方が速いです。
MAL (Make a Lisp)
Lisp方言の一つであると同時に、Lispインタプリタを実装するためのチュートリアルでもあるMALに手を出してみました。この中では珍しく、Lispではなく前々から興味のあったF#で書いています(F#によるMAL実装は既にあったので、esh-fsharpというフォルダに入れています)。
ステップ4まではやりましたが、ステップ5の末尾再帰の実装がなんだか関数型っぽくなくなりそうで尻込みしている*2のと、同時期にLispが楽しくなってきたので頓挫しています。
cl-naive-bayes
単純ベイズ分類器(もしくはナイーブベイズ分類器)です。普段使っているInoReaderに若干不満があった*3ので、RSSリーダでも自作してみようかと思い、その部品として作りました。精度は問わないので、とにかく簡単に使えるものとして作っています。
単純ベイズ分類器としては一通りできあがっています*4。が、新しいおもちゃ下記のps-experiment関係の方が面白くなってきたのでRSSリーダ自体や、それに向けた分類器への機能追加は止まっています。そのうち再開する気でいますん。
kaggle-titanic
現実の統計的課題を世界中のデータサイエンティストが競って解くというKaggleなるものがあります。某氏にそそのかされて、そのチュートリアルとして有名(らしい)なタイタニックの生存者を推定する問題をCommon Lispで解いてみました。
まずは上記のベイズ分類器を試してみて、次はもっとマシな分類器で…というつもりでいました。が、CSVデータ処理マクロを書いたところで満足してしまったので、最初の段階で止まっています*5。テストデータの正解率は77%ぐらいで箸にも棒にもかからない感じです。
マクロの成長過程の一例としては見れそうなので、そのうちたぶん記事にします。
caveman-sample
深町さん作のWebフレームワークCaveman2を使ったサンプルです。HTML直書きやデフォルトのDjulaの代わりにcl-markup、JavaScript直書きの代わりにParenscript、とCommon Lispづくしで書いて見ています。
今のところ、Angular.jsでアコーディオンパネルを作ってみたり(上記のRSSリーダに向けた実験)、WebGL(Three.js)を動かしてみたりして遊んでいます。
ps-experiment
上記のcaveman-sampleを作る中でParenscriptの不満なところに色々マクロを被せていたのですが、分量が増えてきたので独立させました。
ここ数ヶ月は大体これをいじっていた気がします。基本的にはJavaScriptでしかないところはよりJavaScriptらしく、逆にCommon Lispとして書ける部分はよりCommon Lispらしく、という方向で進めています。
cl-ps-ecs
ゲームプログラムのアーキテクチャの一つにEntity_component_system(ECS)というものがあります。「[GDC 2015]エンジンとツールがないなら自作しよう。「World of Tanks Blitz」ローンチまでの道のりを開発者が振り返る - 4Gamer.net]」で知って以来、自分で書いてみたら面白そうと思っていたものです。以前はXNAでオブジェクト指向な簡単なライブラリを書いたりはしてましたが、今さらXNAはないよなあ…と移行先に迷って放置していました。
今やCommon Lispという新天地を手に入れたので、せっかくだからWeb GL(→JavaScript→Parenscript→ps-experiment)でも組み合わせて作ってみようか、と動き出したところです*6。
進捗としては、ようやくcaveman-sampleの方でお試し第一段を書けるようになったところです。が、まだ使える代物には全然なっていません。
特徴として、(今の所)Common LispとParenscript(+ ps-experiment)の共用コードとなっています*7。これについては、実用的な価値を求めたというより、ps-experimentの皮を被せることでどこまでCommon Lispっぽく書けるかを試している感じです。
ちなみにps-experimentの安定化にもかなり貢献しています。なにしろ数歩ごとにps-experimentのバグを2つ3つ踏み抜いていたので…*8。
- 間接的に関連する記事:マクロ展開時に副作用を起こすことの恐ろしさ - eshamster’s diary
その他
ひっそりとScheme(Gauche)やClojureを触ったりもしてましたが、githubにはまだ痕跡がないです。
また、年始にUnityを少し触っていました。簡単なモデルを作って動かしたりユニティちゃんを動かしたりしてみましたが、特に形になったものはないです。プロジェクトがすぐアセットだらけになりますが、こういうMByte級のファイルがGitの履歴に溜っていくのは気持ち悪いですし、かといってバージョン管理なしでは怖くて何も作れない体質になってしまったので、そこで挫折しています。RubyのGemfileのようなものってないんでしたっけ。アセット編集した場合どうするんだとかもあるので、簡単でないのは分かるのですが。一応自作or編集したスクリプトだけ取り出した残骸はありますが、これだけでどうなるものでもないです…。
来年に向けて
Lispという新天地を手に入れたおかげでプログラミングのモチベーションがかなり回復したので、引き続きこの方面を掘っていきたいと思います。Lispは何もかも全てLispで書きたくなる魔力に満ちていますが、他の言語にもまた足を伸ばしてみるつもりです。候補は色々浮かびますが、まあ気の向いたところに進むつもりです。
他にGithub, CircleCI, (ぼっち)Slackと環境の近代化も進めてきたので、引き続き色々なツールを漁っていけたらと思います。
*1:手生成が明らかなボトルネックになっているので、ビットボードに手を出す必要がありそうです
*2:解説を読んだ限りは、ステップ4から差分それだけでいけるのかー、と感心はしたのですが
*3:とはいえ、今は亡きGoogle Readerからの乗り換え先として検討した中では圧倒的に良いものでした
*4:はじめての怪しげな英語ドキュメントを書いてみたりもしました
*5:一応、雑なアンサンブル単純ベイズは書いてみましたが、特に精度は上がらなかった…という所で飽きました
*6:ちなみに、Parenscriptにこだわっているのは、Web GLヘビーに作るとただのJavaScriptになってしまって悔しいので、Common Lispで書ける範囲を確保したいといった辺りが理由です
*7:ps-experimentで"xxx.ps+"系のマクロをいくつか提供しています。例えば"defun.ps+"ではCommon Lispの関数とJavaScriptの関数(を生成するCommon Lisp関数)が生成されます。現状これを利用して全て書いています。肝心のパッケージ周りは作り込めていないので、use(use-package)でお茶を濁していますが…。
*8:テストはきちんと書いているんですけどね…。パッケージや評価順序周りのやっかいなバグが中々つかまらないです