読者です 読者をやめる 読者になる 読者になる

Node.js本のサンプルをCommon Lispで書く

こんな本を買いました。タイトルを見てもピンと来なかったのですが、目次やレビューを見ると新し目の技術を広く浅く見るのに良さそうと感じた次第です。

当然サンプルコードはJavaScriptで書かれているわけですが、そのまま書き写しても面白くないので、前回記事で作った環境上を使ってCommon Lispで書いてみることにしました。Parenscriptを利用して、Common Lispで書いたコードをJavaScriptコードに変換する方針です。

github.com

結果としては、次の手順で作れるようになりました。単一ファイルのスクリプトのみ対応済みです。

  1. init.ros(Roswellスクリプト)でテンプレートからlispファイルを作成
  2. プログラム本体や(Node.jsの)依存ライブラリを記述
  3. run.rosJavaScriptへ変換&実行

テンプレート: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しておく必要があります。

github.com

どんな感じに使えるのかは記事後半のサンプル参照。

テンプレートから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関数の最後の数行です。

  1. init.rosで生成 & 手で編集したlispファイルをロードし(load
  2. JavaScriptファイルをWORKディレクトリ下に書き出し(output-js
  3. 未インストールの依存npmパッケージをインストールし(install-by-pm
  4. 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=鎖のイメージです。ClojureJava関数のチェインで".."が使われているのでなぞるべきだと思ったのですが、Common Lispではエスケープが必要になるので断念…。