八発白中

はてなブログに引越しました。

Cより高速なCommon Lispコードを書く

Cで書くコードの方がCommon Lispで書くより速いって人がいたら、それは彼のCの技量が高すぎるってことだね。

“If you can't outperform C in CL, you're too good at C.”Eric Naggum

最近、Common Lispの非同期Webサーバ「Wookie」を高速化する過程で、ボトルネックになっていたHTTPリクエストのパース部分を高速に処理するライブラリを書きました。

既存のライブラリ「http-parse」よりも約10倍速く、Cのライブラリ「http-parser」より5%ほど高速です。

比較対象のCのhttp-parserはNode.jsで使われているものなので、かなりの高速化が施されていると推測されますが、それをCommon Lispに移植したら少しだけ速くなりました。処理系依存のハッキーなテクニックを使ってるわけでもなく標準の機能だけで。

そこで今回は、Common Lispで高速なプログラムを書くときに僕が考えていることを共有しようと思います。ほとんど我流なので間違っているかもしれません。その場合は指摘してください。

処理系はSBCLを前提としています。

プログラムの設計段階で考慮すべきこと

早すぎる最適化は諸悪の根源である。
“Premature optimization is the root of all evil” — Donald Knuth

「早すぎる最適化」という言葉はプログラマの間で有名です。どこがボトルネックかを知ることなくやたらめったらハックをしていくと無駄にコードが複雑になることを諌める言葉です。

とはいえ、このプログラムは高速に動く必要がある、というときには設計段階で考慮しておかなければならないことがいくつかあります。

その一つがデータ構造です。

データ構造を決める

ルール5: データはすべてを決定づける。もし、正しいデータ構造を選び、ものごとをうまく構成すれば、アルゴリズムはほとんどいつも自明のものになるだろう。プログラミングの中心は、アルゴリズムではなくデータ構造にある。

“Rule 5. Data dominates. If you've chosen the right data structures and organized things well, the algorithms will almost always be self­evident. Data structures, not algorithms, are central to programming.”Robert C. Pike

遅いプログラムは、使っているデータ構造が間違っていることが多いです。一番ありがちなのが、何にでもリストを使うというもの。

しかし、Common Lispは豊富なデータ型があり、それぞれの特性を知り、適切にデータ構造を使い分けられることが高速なプログラムを書く近道です。

List vs. Vector

たとえばListとVectorには以下のような特徴があります。

List

  • 長所
    • 可変長のデータを作るときに向いている
    • 要素の型は何でも良い
    • 部分的に切り出して使うのが楽
  • 短所
    • 長いリストの連結などがONの効率性
    • 長さをとる (length) の速度が線形時間かかる
    • indexを指定したランダムアクセスに向かない

Vector

  • 長所
    • simple-vector (非可変長のベクタ) は高速
    • 要素の型がすべて同じときは型宣言をすることで高速になる
    • indexを指定したランダムアクセスに向く
  • 短所
    • 部分的に切り出して使うみたいなときに大変

高速なプログラムを書くには、可能なら長さ固定で要素の型がすべて同じようなVectorを利用することを検討すべきです。

長さを必ずしも固定にしなくても、1024個のバッファを作って数回イテレートするほうが高速という場合もあります。

Hash Table vs. Structure vs. Class

また、似た比較として、ハッシュテーブル、構造体 (Structure)、クラスの使い分けも重要です *1

Hash Table

  • 長所
    • キーに任意のデータが使える
  • 短所
    • アクセスが遅い
    • 継承はできない

Hash Tableを作る関数make-hash-tableはキーワード引数:testでテスト関数を指定することができます。キーにシンボルしか使わない場合は’eq (ポインタによる比較) を指定するとアクセスが高速になります。

Structure

  • 長所
    • 軽量
    • 高速
  • 短所
    • 継承はできない (:includeを使って他の構造体からfieldだけもらうことはできる)

Class

  • 長所
    • 継承ができる
    • つまりユーザに拡張性を残すことができる
  • 短所
    • どのメソッドやアクセサが呼ばれるかが実行時に判定されるので遅い

Common Lispでは、Structureは内部的には配列になっており、かなり高速です。キーが固定のオブジェクトをいっぱい作るようなときはStructureを使うと良いです。

ただし、これはトレードオフでもあります。

もし書いているプログラムが拡張性を持つ必要があるならClassを使ったほうがいいです。ユーザが独自の継承クラスを定義したり、メソッドモディファイアで挙動を変更できる、ということはプログラムの種類によっては重要です。

データのコピーをするな

せっかく良いデータ型を選んでも、データのコピーを行うと遅いしメモリも食います。

とはいえ難しいのは、どこでデータのコピーが行われるのか、というのが自明ではないこと。

たとえば、subseqは必ずシーケンスのコピーを返します。たとえ(subseq seq 0)と呼び出しても、新しいシーケンスを作ります。既存のhttp-parseが遅い理由の一つはこれでした。

シーケンスの部分データを切り貼りして最終的にシーケンスを返したい、ようなプログラムの場合は「XSubseq」を使うと高速です。

他にはリストに対するappendとかreverseとかもありますね。可能ならnconcnreverseを使えると効率的です。

何はともあれベンチマーク

推測するな、計測せよ。
“Don’t guess, measure!”

書き終わったら、何はともあれベンチマークを取ります。

ベンチマーク

Common Lispには標準でtimeというマクロがあり、実行時間やメモリ・CPU使用率などを表示してくれます。

(time
 (loop repeat 100000 do
   (http-parse parser callbacks data)))

;-> Evaluation took:
;    0.431 seconds of real time
;    0.431906 seconds of total run time (0.430379 user, 0.001527 system)
;    100.23% CPU
;    1,288,960,305 processor cycles
;    0 bytes consed

これは今後何度も実行することになるので、run-benchmarkなどの名前でどこかに定義しておくのは重要です。修正した内容が高速化に役立ったか、むしろ遅くなったかとかが即座にわかります。

また、この数値がどんどん速くなっていくのを見るのは楽しいです。意外と重要。

プロファイル

プログラム内でどの部分に時間がかかっているかを知るにはプロファイルを取ります。

SBCLでは標準でプロファイラが付属しています。

;; プロファイルを取るパッケージ名を指定
(sb-profile:profile “FAST-HTTP” “FAST-HTTP.BYTE-VECTOR” “FAST-HTTP.PARSER” “FAST-HTTP.URL”)

;; 何かコードを実行
(time
  (loop repeat 100000 do
    (http-parse parser callbacks data)))

;; プロファイルレポートを表示
(sb-profile:report)

fast-httpのプロファイル結果は以下のようになっています。

  seconds  |     gc     |  consed |    calls   |  sec/call  |  name  
----------------------------------------------------------
     0.000 |      0.000 |  32,960 | 43,000,000 |   0.000000 | FAST-HTTP.PARSER::CHECK-HEADER-OVERFLOW
     0.000 |      0.000 |       0 |    200,000 |   0.000000 | FAST-HTTP.PARSER::INIT-MARK
     0.000 |      0.000 |       0 |          1 |   0.000000 | FAST-HTTP.PARSER::MAKE-MARK
     0.000 |      0.000 |       0 |          1 |   0.000000 | FAST-HTTP.PARSER:MAKE-LL-CALLBACKS
     0.000 |      0.000 |  81,712 |    100,000 |   0.000000 | FAST-HTTP.PARSER:HTTP-PARSE
     0.000 |      0.000 |       0 |          1 |   0.000000 | FAST-HTTP.PARSER:MAKE-LL-PARSER
     0.000 |      0.000 | 439,072 |    100,000 |   0.000000 | FAST-HTTP.PARSER:HTTP-PARSE-HEADERS
     0.000 |      0.000 |       0 |    800,000 |   0.000000 | FAST-HTTP.URL:PARSE-URL-CHAR
----------------------------------------------------------
     0.000 |      0.000 | 553,744 | 44,200,003 |            | Total

estimated total profiling overhead: 62.41 seconds
overhead estimation parameters:
  6.e-9s/call, 1.412e-6s total profiling, 5.8999996e-7s internal profiling

どれも合計0秒以下なのでちょっとわかりにくいですが、上から順に時間がかかっている関数が表示されています。GCにかかった時間、メモリの使用量 (consed)、何回呼ばれたか、呼び出し1回あたりの実行時間が並んでいます。

言うまでもなく、対処するべきは上の方にある関数からです。

まずはcallsの欄を見て、その関数が自分の予想を超える回数呼び出されているとしたら、それを減らす方法を考えます。無駄なループなどが見つかるかもしれません。

型宣言とoptimize

ここからはプロファイラの結果に基いて、個別の関数を最適化する方法です。

けど、楽してプログラムが高速になるなら、それほどうまい話はないですよね?

適切なデータ構造を使っている場合、型宣言を追加するだけでかなり高速になることがあります。

特に、「数値」と「配列」に関しては顕著です。

もし変数の数値がfixnumの範囲 *2 に収まるなら(declare (type fixnum 変数名))とすると、数値演算や比較の関数がコンパイル時に決定できるので高速になります。

また、配列がsimple-vector (非可変長) の場合、要素の型がすべて同じ場合なども型宣言はかなり有効です。

たとえば (unsigned-byte 8) のsimple-vectorは以下のように宣言します。

;; 長さ不定の非可変長のバイトベクタ
(declare (type (make-array (unsigned-byte 8) (*)) 変数名))

長さも固定の場合は以下のように書けます。

;; 長さ3の非可変長のバイトベクタ
(declare (type (simple-array (unsigned-byte 8) (3)) 変数名))

型宣言をしたらoptimize宣言も追加します。

(declare (optimise (speed 3) (safety 2)))

speedは速度優先でコンパイルsafetyは実行時に型チェックを行うかどうかを表しています。(safety 0)にすると実行時に型チェックを行いません。

Common Lispにはlocallyというスペシャルフォームがあり、部分的にdeclareすることもできます。通常は(safety 3)にしておき、ホットスポットのみ(safety 0)にしたりできます。Common Lispの便利なところですね。

関数のインライン化

関数自体は高速だけど、呼び出し回数が多い場合は関数呼び出しコスト自体が高くつくことがあります。

そういう場合は関数をインライン宣言することで呼び出しコストをゼロにできます。

(declaim (inline 関数名))

ただし、インライン化するとプロファイル結果に出てこなくなりますし、デバッグもしづらくなるので適度に使うべきです。

コンパイル時までに計算できることはする

ここからはちょっと大変で、プログラムによって最適化の方法が変わってきます。

一つの方針として、「コンパイル時までに決定できることは決定しておく」という方法があります。

たとえば以下のような関数があるとします。

(defun invoke-callback (parser name)
  (let ((fn (symbol-function (intern (concatenate 'string "PARSER-" (symbol-name name))) parser)))
    (when fn
      (handler-case (funcall obj)
        (error (e) (princ e *error-output*))))))

(invoke-callback parser 'body-cb)

実行時にシンボルをinternしてfuncallしています。しかし、引数が定数なので、internまではコンパイル時に解決できます。同等のマクロとして再定義したほうが高速です *3

また、もっと小さな範囲で解決したい場合はリードマクロの#.が使えます。

これは、後に続く式をリード時に実行し、その結果を式に埋め込むものです。リード時に解決できるものならこちらが使えます。

(= byte #.(char-code #\A))

(番外編) 高速なライブラリ

せっかく高速なプログラムを書いたのに、使っているライブラリが遅いせいで全体が遅くなる、という場合があります。そのため使うライブラリが高速かどうかを知っておくことは重要です。

たとえば以下の3つのライブラリは高速に動作します。

ただし、fast-ioを使えば必ずしも高速というわけではなく、長さ不定のバイトベクタを作る場合などに効いてきます。

XSubseqはsubseq + concatenateを連続で行うような場合に使えます。

JSONライブラリではcl-jsonやyasonなどがありますが、これらは拡張性を優先しているため遅いです。高速なJSON処理が必要な場合はjsownが使えます。

高速なライブラリを見分けるには

ライブラリが高速かどうかを見分けるのに僕が見ているポイントなどを書いておきます。

  • クラスを使っていないか

クラスを使っているライブラリは実行速度より拡張性を優先しています *4

flexi-streamsを始めとしたGray Streamsはメソッド呼び出しを行うので遅いです。

ベンチマーク実行用のコードがあると、そのライブラリは速度を重要視して書かれている可能性があります。

おわりに

僕はこんな感じで高速なプログラムを書いています。

他の言語だと高速なプログラムはCで書いてFFIで繋ぐみたいなことをしないといけないけど、Common LispだとCommon Lispで書いても高速なのがいいですね。

最適化って、努力した分だけ利くってわけでもないのでつらいですが、最終的に高速なCommon Lispコードが書けるとかなり脳汁が出るのでおすすめです。

参考

*1:Property listやAssociation listのような、オブジェクトをlistで代替する方法もありますが、言うまでもなく遅いので無視します

*2:SBCL 1.2.4では-4611686018427387904〜4611686018427387903

*3:同等のことをコンパイラマクロですることもできます。関数として定義しておきたい場合はコンパイラマクロを使います。

*4:もちろん、クラスを使っていてもコンパイル時に走るコード (マクロ内でしか使われていないとか) なら問題ありません。

自分のTwitpicの画像・動画をダウンロードできるスクリプトを書きました

Twitpicの公式ブログで、Twitpicが今月終了することが告知されているようです。

追記 (2014/09/19): Twitpicが買収されたので終了しない、ということを公式Twitterアカウントでツイートしています。 詳細は今後公開されると思いますが、終了に向けてのダウンロードの必要はなくなりそうです。
追記 (2014/10/17): Twitpicの買収が失敗したようで、再び終了する告知が出ています。

僕は昔少し使っていた程度ですが、それでもサービスが無くなるのは寂しいですね。ブコメで誰か書いているのを見ましたが、こうしてデータが消えていくと文化が後世に残らないので数十年後困ると思う。

ともかく今月の話なので、昔の写真が上がってるからダウンロードしておかないとなー、と思ってバックアップ用スクリプトを書いてみました。

※ちなみに終了告知には「数日中にデータのエクスポート機能を提供する予定」、と書かれているので待っていれば公式のダウンロード機能が使えそうです。

追記 (2014/09/09):

Twitpic公式で写真/動画のエクスポート機能が追加されました。


設定ページの一番下でエクスポートリクエストができ、準備ができたらダウンロードリンクが表示されるようです。


ダウンロードしてみたら、写真や動画だけでなく投稿時のツイート内容がtweets.txtに保存されています。それに倣って僕のスクリプトも同様の内容を出力するようにしました。


投稿日時情報がファイル名に含まれていないのが残念かな……重要だと思うんだけど。

重要そうな機能

  • 動画にも対応しています
  • 投稿日時がファイル名になっているのでいつ投稿したかがわかります
  • ファイル名の最後5ケタはTwitpicのIDです
    • http://twitpic.com/{ID} にアクセスすると対応するTwitpicのページが見られます。
  • 投稿時のコメントをテキストファイルに出力します

必要なもの

使うにはCommon Lisp処理系とQuicklispが必要です。

Common Lisp処理系

特にこだわりがなければSBCLClozure CLがおすすめです。

Mac OSならHomebrewで簡単にインストールできます。

$ brew install sbcl

Ubuntuならapt-getでインストールできるはずです。

$ sudo apt-get install sbcl

Quicklisp

Quicklispをインストールします。

公式サイトの言うとおりにインストールしてもいいですが、以下のコマンドを叩けば一発でインストールできます。

$ (curl -L http://beta.quicklisp.org/quicklisp.lisp && echo '(progn (quicklisp-quickstart:install) (ql:add-to-init-file))') | sbcl

ダウンロード

以下のリンクを右クリックして「リンク先を保存」します。

実行

以下のコマンドを実行します。twitpic-dl.lispの部分は先ほどダウンロードしたファイルのパスです。

$ sbcl --noinform --load twitpic-dl.lisp

f:id:nitro_idiot:20140906105714p:plain

するとユーザ名を聞かれるので自分の名前を入れてEnterを押します。

f:id:nitro_idiot:20140906105549p:plain

ダウンロードが始まります。ファイル名は投稿日時になっています。途中サーバエラーが出た場合は間隔をおいて自動でリトライします。

Ctrl-Cでいつでも終了できます。途中で終了した場合、再度実行すればダウンロードしていないファイルのみダウンロードを再開します。

何か問題があったら@nitro_idiotにご連絡ください。

ソースコード

Gistに上がっています。ライセンスはPublic Domainなのでご自由にご利用ください。

Twitpic backup script

どこでCommon Lispの質問をしたらいいのか

1週間ほど前、Quicklispの作者のZach Beaneがこんなブログ記事をポストしていて少し話題になりました。

Common Lispの質問をする場所はいくつかある。

一般的な質問の場合、

  • Stack Overflowに「Common-Lisp」タグをつけて明瞭な質問をすれば、何人かの詳しい人が即座に回答してくれる。特にRainer Joswigは数百の質問に質の高い情報を提供しているし、他にも多くの人がすばやく回答をくれる。回答はスコア付けされ、一般的には良い回答はスコアが高く、悪い回答はそれなりになる。
  • reddit/r/lisp/r/learnlispに投稿された質問も同様に多数の回答がつけられる。経験上、redditにはより多くの人がいて回答をくれる。個人的には未熟な質問でも良いと思う。回答はスコア付けされ、悪い回答は通常downvoteされる。
  • FreenodeのIRCチャンネル「#lisp」はリアルタイムのやり取りに向いている。たとえ初心者レベルであっても質問は歓迎されるが、良い本を読んでハマったなどの場合であれば尚良い。あなたがもし退屈していて他のIRCの友達と話したいと思っていたとしても、議題は大事にされる。話題に任せて進行して、ときどきCommon Lisp以外の話題は小言を言われる。スパマーや荒らしはkickされてbanされる。回答の質によっては評価されない。5人が一度に各々のビジョンを語れば最も熱意のある (もしくはタイプの速い人) が優位になる。ときどき誰の話を聞けばいいのかわからなくなる。とはいえ素早いIRCフィードバックループはその欠点を差し引いても価値がある。
  • MLのpro@common-lisp.netはいくらか高度な議論がCommon Lispの「プロ」たちによって交わされている。このMLについての公式のお知らせは見つけられないが、概要はCommon Lispを専門的な分野で使っている人々向けのMLで、コンスやマクロがどう動くかについて知りたい人向けではない。
  • 使ったことはないけど、「lispforum.com」も活発のようだ。
  • comp.lang.lispにも質問を読んで質問してくれる人々がいくらかいる。質問はまったくスコア付けされない。WJやgavinoなどの荒らしが数年に渡って投稿を続けている。S/N比が酷いこともある。残っている人々は、どうkillfileを管理するかわかっている人か彼らに罰を与えることに熱心な人だけだ。Google Groups上で読んだり投稿してはいけない。整形された投稿を台無しにしてしまうし、記事のフィルタリングの邪魔になる。私はnews.individual.netからアクセスしていてコストはとても低い。他にも無料のサーバがある。私は投稿を読んだりフィルタリングするのにEmacs用のGNUSを使っている。

その他、処理系別のメーリングリストやQuicklispのメーリングリストが紹介されています。


今年の1月に、「Common LispのStack Overflowの質問が少なすぎる」と話題になったけど、ここではStack Overflowが一番に挙げられるんだなぁと思った。一番良質な回答が得られるってことかな?

個人的ノウハウとして、何か質問したいなー、というときは、とりあえずTwitterに投稿する。すると即座に@snmstsという人が回答してくれる。

日本語で質問できる場所

基本的にどこに行っても英語です。日本語でCommon Lispの質問できる場所・されている場所っていうのは無い気がしています。Twitterで困ったツイートして誰かにキャッチされるのを待つとかでしょうか。

reddit/r/lisp_jaで質問するのも良いかもしれないですが、質問を見たことはありません。

Gaucheの質問であればChaton Gaucheが活発のようです。Kawai Shiroさんが直々に回答してくれます。Common Lisp用のChatonも、やや不活発ではありますが利用されています。

Shibuya.lispのMLにも質問を投げていいはずです。気づけば運営の報告メーリングリストになっていて投げづらい雰囲気ではあるけど。

最近はShibuya.lispが月一イベントの「Meet Up」を開催しているので、直接会って聞くのもありっちゃあり。

僕もしょっちゅうバグ報告をもらう。そういえばもらうのは質問じゃなくてだいたいバグ報告だった。

他に何か良い場所があったら教えて下さい。

追記 (2014/09/09): コメントでtitoshiさんに、2chCommon Lispスレッドも情報交換に使われていると教えていただきました。

Shibuya.lisp TT #8 で「Redesigning Common Lisp」という発表をしました

先日8/30(土)、Shibuya.lisp Tech Talk #8 が開催されました。

イベント概要

イベントではTechnical Talkが5つ、Lightning Talkが9つありました。

参加者は全部で57名。よく使うLisp方言のアンケートを取ったら、だいたいCommon LispSchemeClojureが1/3ずつくらいばらけていました印象です。コミュニティ内に多様性があるのは非常に良いですね。

各発表も話題が多様でした。

とても内容の濃い一日だったと思います。

途中@nobkzさんの発表で、「弊社、Lisperを募集しています!」という求人募集もありました。いいですね、こういうの。

発表しました

僕もTechnical Talkで「Redesigning Common Lisp」と題して40分程度の発表を行いました。

新しくLispエイリアン色のプレゼンテーマを作って使ってみました。

内容は僕が今年始めたプロジェクトの一つの「CL21」についてです。

質問や意見も多くいただきました。ありがとうございました。

話さなかったこと: CL21のこれから

時間の関係と少し話がそれてしまうという理由で削ってしまったことについて補足します。

CL21はこれからCommon Lispの再整理を行い、より小さな言語カーネルを目指す予定です。

f:id:nitro_idiot:20140901113406p:plain

Richard GabrielのWorse Is Betterという記事に、言語を4層に分離するという方法が提案されています。

言語は少なくとも4つの層に分割されるべきだ:

  1. 言語カーネル。この層は単純で実装しやすいものとなる。どんなケースにおいても、動的な再定義は再考を加えて、このレベルでのサポートが必要かどうかを判断するべきだ。私は再定義可能なものがカーネル内に必要だとは思わない。
  2. 言語を肉付けする言語学的な層。この層は実装上の困難が若干伴うかもしれない。そしてここには多分、カーネルで実装するには高価すぎるが割愛するには重要すぎる動的な側面が含まれることになる。
  3. ライブラリCommon Lispにあるものの大半はこの層に置かれる。
  4. 環境として提供されるエピ言語的機能

1番目の層に私は、条件式、関数呼び出し、すべての基礎データ構造、マクロ、単値、非常に基礎的なオブジェクト指向のサポートを含める。

2番目の層に私は、多値とより進んだオブジェクト指向サポートを含める。2番目の層は、環境が提供するに任せるにはあまりに重要すぎる、難易度が高いプログラミング上の構造、しかしそれでいて正確な定義を正当化するだけの意味論的な重要性が十分にある、そういうもののためにある。何らかの形の再定義機能はここに置かれるかもしれない。

3番目の層に私は、シーケンス関数、手の込んだ入出力関数、その他1番目の層と2番目の層に単純に実装できなかったものすべてを含める。これらの関数はリンク可能であるべきだ。

4番目の層に私は、環境が提供でき、またそうすべきであるが、標準化されなければならない機能を含める。典型的な例はCLOSのdefmethodだ。CLOSでは、総称関数はメソッドから成っており、各メソッドは特定のクラスに適用可能である。1番目の層には完全な総称関数のための定義式――つまり総称関数およびその全メソッドの定義が一箇所に集まったもの――がある(第1層のコンパイラがそれらをどう見たいかを反映している)。名前を総称関数に結び付ける手段も用意される。しかしながら、システムを発展させていく途上で、クラスはさまざまな場所で定義され、関連した(適用可能な)メソッドがクラスの隣に見えたほうが道理にかなっている。メソッドを作るその仕組みはdefmethodで、defmethod式は他の定義式の中のどこにでも置ける。

この方法には2つのメリットがあります。

  • 何をどこのレイヤーに入れるべきか、または入れないべきかの議論の基準が明確になる
  • CL処理系の実装が楽になる

CL21における議論で、「この機能を追加してよ」というものはかなり多いですが、それがどの層に属するのか、同じレイヤーの他の部品と比較して汎用性は十分にあるか、などより具体的に話し合うことができます。

もう一つのメリットである処理系の実装を簡単にするのは、実行環境が(再び)多様化しつつある昨今で、実行環境が生き残っていくために必要かなと考えています。

たとえばJSCLというJavaScriptで実装されたCL処理系がありますが、JavaScriptという独特の難しさを抱えており、CLの仕様をすべて満たしきれないでいます。この視点での言語の4層分離は必要そうです。

CL21の現状

こういう話を踏まえて、CL21の現在のステータスを話すと、「約束できることは何もありません」。

まだ議論も続いていて、何もFixできる状態でなく、早々の安定化を目指すよりも本来の目的である「Experiment (実験)」することの意義を優先しています。

何か良いアイデアをお持ちの方はGitHub Issuesでの議論に参加してください。

まとめ

Shibuya.lisp Tech Talk楽しかったですね。運営スタッフの方々お疲れ様でした。会場を貸していただいた株式会社mixiの鈴木さん、ありがとうございました。

また、Shibuya.lispは月一のMeetUpイベントも開催しています。

次回は来月下旬に開催予定なので、興味がある方はぜひ参加を検討ください。

プロジェクトのCommon Lispライブラリ管理ツール「qlot」を作りました

想像してください。

Common Lispで開発しているアプリケーションが手元にありますよね。

それを他の環境、たとえば他の開発者のPC、CI環境やWebサーバなどで動かしたいというときに起こる問題はなんですか。

大きな問題は処理系やライブラリのバージョンが違うために、自分の環境では動くのに他の環境に持っていくと動かないということです。

処理系に関してはCIMを使えば固定できますが、Common Lispにはライブラリのバージョンを固定する方法は現状ほとんど無いために問題になります。

こういった、Common Lispプロジェクトの可搬性を上げるツールとして「qlot」を作りました。

Quicklispの何が問題か

Common LispにはQuicklispという偉大なライブラリインスーラ及びライブラリの中央リポジトリがあります。依存ライブラリも含めてインストール・ロードしてくれるので、Common Lispでの開発が非常に楽になりました。

けれど、Quicklispにはいくつか問題があります。

  • 更新が月一しかない
  • 特定のライブラリのみ違うバージョンを使うことができない

更新が月に一回しかないので、GitHubに上がっている最新のClackを使いたいみたいなことができません。

また、Quicklispはライブラリを毎月のアップデートごとに管理しているので、すべてのライブラリを同じ時期のものを使うしか選択肢がありません。つまりlog4clだけ古いバージョンを使うということもできません。

"local-projects/"やASDFの設定をいじればgit cloneしたライブラリを使うことはできますが、git cloneしたことを忘れてて更新があったあとも古いバージョンをロードしてしまうみたいなことないですか。僕はつい最近もnamed-readtablesでやらかしました。

とにかく、Quicklispは使うライブラリを細かく管理するという目的ではまったく使えないのです。

qlotとは

qlotは、Common Lispライブラリをプロジェクトローカルにインストール・ロードすることができるツールです。

(正確にはプロジェクトローカルにQuicklispをインストールします。)

使い方を見せたほうが早いのでとっとと使い方を紹介します。

使い方

qlfileを置く

プロジェクトで使うライブラリをqlfileに記述します。すべてを記述する必要は無く、書かれていないものであれば最新のQuicklispのdistからロードされます。

たとえばGitHubにある最新のClackとdataflyを使いたい場合は以下のようなqlfileを用意します。

git clack https://github.com/fukamachi/clack.git
git datafly https://github.com/fukamachi/datafly.git

詳しいqlfileの書き方はGitHubのREADMEを参照してください。

単純にquicklispをプロジェクトごとに分離したい場合は空のqlfileを用意してください。

ライブラリをインストールする

qlotを通常通りにql:quickloadでロードします。

(ql:quickload :qlot)

それからqlot:installをプロジェクト名と一緒に呼び出します。

(qlot:install <プロジェクト名>)
(qlot:install :myapp)

:myappであれば(qlot:install :myapp)みたいな感じです。

インストールはこれだけです。プロジェクトルートにquicklisp/ディレクトリとqlfile.lockファイルができているのが分かると思います。

qlfile.lockはインストールした内容をスナップショットとして記録したものです。このファイルがあるとqlot:installはlockを優先します。一度こうしてインストールしておけば、他の環境でqlot:installしたときにも全く同じバージョンのライブラリを使うことが保証されます。

quicklisp/VCSから無視されるようにし、qlfileqlfile.lockを追加しておきます。

$ echo quicklisp/ >> .gitignore
$ git add qlfile qlfile.lock
$ git commit -m 'Start using qlot.'

プロジェクトをロードする

プロジェクトをロードするときはql:quickloadの代わりにqlot:quickloadを実行します。

(qlot:quickload :myapp)

これはプロジェクトローカルのQuicklispを使ってロードする関数です。

ライブラリをアップデートする

一度qlot:installしたあとqlfileを更新したり、ライブラリのバージョンを更新したいときなどはqlot:updateを実行します。

(qlot:update :myapp)

これでquicklisp/の内容とqlfile.lockが更新されます。

デプロイするとき

qlfile.lockさえ環境間で共有しておけば、qlot:installを実行するだけで同じQuicklisp環境が再現されます。

(qlot:install :myapp)

まとめ

使う関数はqlot:installqlot:quickloadqlot:updateの3つだけです。簡単でしょ?

今月のQuicklispアップデートで入る予定なので、試したい方は1、2週間くらい待つと使えるようになると思います。

ソースコードは例によってGitHubに上がっています。Starしてね!

余談: Lispイベント

今月は中旬にモントリオールでInternational Lisp Conferenceがあったり、月末にShibuya.lispのTech Talkがあったりでなかなか良い月になりそうですね。

Shibuya.lispテクニカル・トーク #8
日時: 8/30(土) 11:00 〜 18:30
場所: 株式会社ミクシィ

トークの応募は8月16日(土)までなので、何か話したい人は早めに準備して応募すると良いと思います。

僕も、qlotについてではないですが他のプロジェクトについて話すつもりでいます。みんな参加しましょう。