Hatena::ブログ(Diary)

八発白中

2012-04-19

なぜ僕はcl-annotを使うのか

Twitterで、なぜcl-annotを使うのかという主旨の質問を受けて、そういえば id:m2ymModern Common Lispに書くだろうからと言ってほとんど利点をまとめてなかった気がします。140字で返信するのは不可能なのでついでにブログに書いておきます。

cl-annotとは

Common Lispアノテーションを使うためのリードマクロライブラリです。

代表的なものに @export があります。

@export
(defvar *default-fizzbuzz-count* 20)

@export
(defun fizzbuzz (&optional (n *default-fizzbuzz-count*))
  (loop for i from 1 to n
        if (zerop (mod i 15))
          collect 'fizzbuzz
        else if (zerop (mod i 3))
          collect 'fizz
        else if (zerop (mod i 5))
          collect 'buzz
        else
          collect i))

使い方は見ての通りで、通常の変数や関数宣言の前に @export とつけるだけです。cl-annotはその他にdefmacroやdefstruct、defclass、defmethodなども当然のように対応しています。

cl-annotはClackCavemanを始め、CL-DBI、CL-Projectなど、僕が作ったライブラリのほとんどで使われています。

なぜcl-annotを使うのかを急に話してもきっと意味がわからないので、cl-annotの特徴から順を追って説明したいと思います。

一般性と透過性

まず重要なのは、その透過性です。リードマクロと言うと、特別なシンタックスを持ち出していると思われがちですが、cl-annotはもっと一般的で単純な変換ルールを持っています。

たとえば @export は以下のように展開されます。

@export
(defvar *default-fizzbuzz-count* 20)

;; ↑は↓のようになる

(progn
  (export '*default-fizzbuzz-count*)
  (defvar *default-fizzbuzz-count* 20))

また、@export はcl-annotが提供しているアノテーションですが、実は一般的なあらゆる関数、マクロ、スペシャルフォームもアノテーションとして書くことができます。

@print 'nitro_idiot
;-> NITRO_IDIOT
;=> NITRO_IDIOT

@quote hatena
;=> HATENA

これは、「@」のあとに書かれたものが特別に定義されたアノテーションでない場合、引数1つを取るS式として変換するためです。

@print 'nitro_idiot
;<=> (print 'nitro_idiot)

@quote hatena
;<=> (quote hatena)

実はもともと @export アノテーションも同様の扱いをしていました。関数の場合は以下のように変換されたとしても動作します。

(export
  (defun fizzbuzz (&optional (n *default-fizzbuzz-count*))
    (loop for i from 1 to n
          if (zerop (mod i 15))
            collect 'fizzbuzz
          else if (zerop (mod i 3))
            collect 'fizz
          else if (zerop (mod i 5))
            collect 'buzz
          else
            collect i)))

ただ、defclassがsymbolではなくクラス自身を返すため、一度マクロを挟んでいるわけです。

拡張性がある

@export はcl-annotが提供する、標準アノテーションですが、自分が欲しいアノテーションがあれば自由に定義することもできます。

たとえば、Cavemanでは @url という独自のアノテーションを定義しています。

@url GET "/:member/:id"
(defun member-profile (params)
  (render "profile.html" params))

これは以下のように定義されています。

(defannotation url (method url-rule form)
    (:arity 3)
  `(progn
     (add-route ,(intern "*APP*" *package*)
                (url->routing-rule ,method ,url-rule ,form))
     ,form))

ほとんど通常のマクロと同じですが、:arity で、いくつの要素を後に取るかを指定できます。

意味論としてのcl-annot

さて、cl-annotの特徴を一通り述べましたが、それでもまだなんでcl-annotなのかというのはわからないかもしれません。別にアノテーションでなくても、Lispならマクロを使えばいくらでもできます。というか、リードマクロを結局マクロに展開しているだけなので、マクロを直接使うのも得られる結果は同じです。

たとえば、Cavemanの @url は 以下のような define-route というマクロを提供してもいいわけです。

(define-route member-profile "/:member/:id" (params :request-method :GET)
  (render "profile.html" params))

ただ、このコードは凡庸なだけでなく、新しいマクロを持ち込むことによって読み手を混乱させ、新しいフォームの使い方を覚える必要があります。

一方、@urlを再掲します。

@url GET "/:member/:id"
(defun member-profile (params)
  (render "profile.html" params))

@urlアノテーションのメリットは、通常のdefunを使えるということです。@urlも効果としてはマクロには違いないのですが、defunを使うことで、読み手は新しいマクロを覚える必要もなく、さらにコントローラの正体は単なる関数であり、member-profileという関数が定義されるのだということが自明になります。

@exportにしても、defun+みたいなマクロを定義して使っている人もいます。ただ、defun+はexportするだけのdefunなのか、それとも他にも副次的な効果があるのか見ただけでは不明です。

もちろん、実態はただのマクロには違いないのですが、受け取るコードの意味を変更せず尊重するという、書き手の特殊な表現方法がアノテーションなのです。

まとめ

cl-annotを使うと便利とか速いとか、そういったことは全くないのですが、コードの表現方法を1つ提供しているという点で僕は素晴らしいライブラリだと思っています。もっと多くの人に使われていろんなアノテーション定義が出てくると面白いんですけどね。

2012-04-03

Common Lispの軽量フレームワーク「ningle」を作りました

Clackベースの軽量Webフレームワークningle」を作りました。

背景

Clackベースのフレームワークとしては既に「Caveman」がありますが、CavemanはPerlのAmon2に影響を受けたこともあり、プロジェクトの拡大に伴う拡張性を損なわないために多くのことをします。

たとえばCavemanにはprojectという概念があり、開発環境と本番環境でロードするconfigファイルを分けたり、どのようにビルドするかを定義したりできるようになっています。

複数のClackアプリを定義することもでき、最初はPCサイトのみ作っていたけど半年後にスマートフォン用サイトも作ることになった、という場合などにも、アプリの継承などですぐに対応できるようになっています。

Clackのミドルウェア利用も記述を少し追加するだけです。

けれど、これらの拡張性のために、最初にプロジェクトのスケルトンを生成する必要があります。これでは試しながらちょっとずつ作っていきたいという気軽さが欠けます。

ningleは、プロジェクトのスケルトン生成をすることなくWebアプリを書くことができる、Cavemanよりさらに薄いWebアプリケーションフレームワークです。

特徴


  • ルーティング以外余計なことしない
    • リクエストとレスポンスをパースしてディスパッチするくらい
  • Clackを生で使いたいときも楽
    • 逆に言えばCavemanほど綺麗にClackをラップしていないのでClack慣れしてないと難しい
  • 妙なマクロ使わないからわかりやすい
    • cl-annot嫌いな方も安心

使い方


git cloneしてロードしたあと以下のような感じで書けば http://localhost:5000/ で起動します。

(defvar *app* (make-instance 'ningle:<app>))

(setf (ningle:route *app* "/")
      "Welcome to ningle!")

(setf (ningle:route *app* "/login" :method :POST)
      #'(lambda (params)
          (if (authorize (getf params :|username|)
                         (getf params :|password|))
            "Authorized!"
            "Failed...Try again.")))

(clack:clackup *app*)

Cavemanでは @url アノテーションのために多少トリッキーなことをしていて、アプリケーションを意識しなくても使えるようになっていますが、ningleでは見ての通り *app* がむき出しです。

ルーティング記述も、他のフレームワークと違って、アノテーションdefine-route のようなマクロではなく、単なる setf を使っています。マクロを使わずに最も自然に書くにはどうすればいいかと考えた末にこの形になりました。とても気に入っています。

最後には、単純にclackupするだけです。*app*に直接アクセスできるので、ここでClack.Builderを使ってClackミドルウェアを有効にすることもできます。

(import 'clack.builder:builder
        'clack.middleware.session:<clack-middleware-session>)

;; セッション機構を有効に
(clack:clackup
  (builder
    <clack-middleware-session>
    *app*))

どう?

Quicklisp

申請中です。

まとめ

まだ実験段階ではありますが、ベースはClackですし、Cavemanからの流用も多いので十分実用的な品質かと思います。興味がある方はぜひ触ってみてください。

2012-04-01

Kyoto.lispを作ろうとしています

京都近郊のLispコミュニティ「Kyoto.lisp」というものを作ろうとしています。

こういうのは「作った」って言えば作ったことになるのか、何人以上集めたらコミュニティなのかよくわからないですが、ブログとかメーリングリストとかは用意してみました。

京都は最初のCommon Lisp処理系が生まれた場所ですし、コミュニティの1個くらいあってもよいのではないかと思います。

今年はILC2012が京都で開催されますし、それに先駆けて1回くらいTech Talkしておきたいですね (5月か6月か)。

2012-03-23

Common LispのWebアプリケーションを社内運用してみた

今月の初めに弊社はてなで開発合宿を行いました。2泊3日の合宿の中でチームを組み、テーマを決めて開発をし、最後に各チームがプレゼンをする、というものです。成果物は今後のサービス開発に生かされます。

僕のチームはバックエンドがCommon Lisp、フロントエンドがCoffeeScriptで、お互いが独立していてAPIでのみ通信する設計のWebアプリケーションを作りました。僕とhitode909とswimy1113の3人の最小チームでしたが、最後のプレゼン投票で優勝できました。

↓ 優勝したときの図。

f:id:kiyohero:20120302113248j:image

左の灰色のはてなパーカーを着てるのが僕です。

Common Lispで書かれたアプリケーションが社内1位ってのはかなり夢があります。

合宿が終わってからも継続して開発を続けており、そろそろ数週間が経ちました。ので、この辺りでCommon LispでWebアプリケーションを作って運用した話をざっくりまとめておこうと思います。何を作ったかは秘密なのでアプリケーション側の話ではなく、どちらかというと運用側でハマったことと解決法が多めです。

開発環境


処理系はいつも使い慣れているClozure CLを使いました。Emacs、SLIMEは当然のようにセットアップされています。使用ライブラリは以下です。

Cavemanは、Clackの上に作られた極小フレームワークです。

Cavemanはまあ当たり前のように使うとして、CL-DBIは今回が初の使用でした。いくつか細かいバグを踏みつつ直しつつで開発を進めました。SQLは直書きして流しこむというスタイルで、最初はSQLite3を使ってちまちま動かしていきました。SQLiteからMySQLへの移行も経験しましたが、ソースコードはなんの変更もなく移行することができました。

テンプレートエンジンには一番癖のないCL-EMBを使いました。

ここで少しハマったのはClozure CLではファイルの読み込みの文字コードのデフォルトがlatin-1になっていて、テンプレートに日本語を書くと文字化けすることです。これは.ccl-init.lisp文字コード指定しておくと回避できます。

(setf ccl:*default-external-format*
      (ccl:make-external-format :character-encoding :utf-8
                                :line-termination :unix)
      ccl:*default-file-character-encoding* :utf-8
      ccl:*default-socket-character-encoding* :utf-8)

サーバ構成


アプリケーションサーバとしてHunchentootを5000番で動かし、Apacheをリバースプロキシとして設置しました。こうするとアクセスログはApache側で取れるし、各種設定もしやすいのでおすすめです。

<VirtualHost *:80>
  ServerName i.love.common-lisp.com

  ProxyPass        / http://localhost:5000/
  ProxyPassReverse / http://localhost:5000/
  ProxyRequests    Off
</VirtualHost>

これで i.love.common-lisp.com の80番にアクセスがきたときに5000番にフォワードします。

デプロイ


サーバは前から持っていたさくらVPS 512MBを間借りして使いました。処理系は実行速度を考えてSBCLで動かします。特に処理系依存のライブラリを使っていないので、Clozure CLからの処理系移行に伴う問題はまったくありませんでした。

git pushしてサーバにアップロードし、SSHで繋いでREPLを起動してサーバを立ち上げれば完成です。

……まではいいのですが、その後SSHを切断するとサーバも一緒に落ちてしまいます。どうやらCL処理系はREPLがベースなので標準入力がなくなるとエラーで落ちるようです。Perlなどでははまらないような罠ですね。どうにかデーモン化しないと。

CLのデーモン化にはかなり手こずりましたが、しばらくしてSBCLでsave-lisp-and-dieを使って実行ファイルを作っておくと容易にデーモン化できることに気づきました。

(ql:quickload :my-application)

(defun main ()
  (my-application:start :mode :prod :debug nil :port 5000)
  ;; 起動を維持するために無限ループする
  (loop (sleep 60)))

(sb-ext:save-lisp-and-die "my-application" :executable t :toplevel 'main)

これで普通に & をつけて実行するとサーバが起動します。

$ my-application &

これでSSHを切断してもサーバは起動したままです。ただ、これだと毎回実行ファイルを作ってサーバを再起動しなければ反映できません。そのため、中でswank-serverを立ち上げてREPLにアクセスできるようにしておきます。

(ql:quickload :my-application)
(ql:quickload :swank)

(defun main ()
  (my-application:start :mode :prod :debug nil :port 5000)
  (swank:create-server :port 4005 :style :spawn :dont-close t)
  ;; 起動を維持するために無限ループする
  (loop (sleep 60)))

(sb-ext:save-lisp-and-die "my-application" :executable t :toplevel 'main)

これで4005番でswank-serverが立ち上がりました。ローカルのEmacsからリモートのREPLに接続するには、ポートフォワードします。

$ ssh -N -f -L 4005:localhost:4005 xxx.xx.xx.xxx

これでEmacsから M-x slime-connect 127.0.0.1 4005 すると本番サーバのREPLにアクセスできます。これでソースをpushした際にモジュールを再ロードするだけで反映できます。

もちろんSLIMEなので、ローカルで編集したファイルの一部分だけをC-c C-cで本番に即座反映することもできます。デバッグにはとても役立ちますが、コミットを忘れてサーバを再起動しなければいけない際にちゃんと立ち上がらない、みたいなことが冗談抜きで本当にあるので注意してください。本当にあります。

最初は上のような書き捨てのスクリプトで何とかやりくりしていたのですが、最近では以下のようなMakefileを用意しています。

build: clean my-application

my-application:
	sbcl --noinform --disable-debugger \
	--eval '(ql:quickload :my-application)' \
	--eval '(ql:quickload :swank)' \
	--eval '(defun main () (my-application:start :mode (or (sb-ext:posix-getenv "CLACK_ENV") :dev) :port (or (parse-integer (string (sb-ext:posix-getenv "SERVER_PORT")) :junk-allowed t) 5000)) (swank:create-server :port (or (parse-integer (string (sb-ext:posix-getenv "SWANK_PORT")) :junk-allowed t) 4005) :style :spawn :dont-close t) (loop (sleep 60)))' \
	--eval '(sb-ext:save-lisp-and-die "my-application" :executable t :toplevel (quote main))'

clean:
	if [ -f my-application ]; then (rm my-application); fi

起動時にCLACK_ENV, SERVER_PORT, SWANK_PORTなどの環境変数を設定すると起動パラメータが変わって便利です。

サーバ構成の変更


社内リリースをしてしばらく運用すると、Hunchentootがたまにレスポンスを返さないことがあるのに気づきました。たまにだけど。Apache Benchをかけてみると、Hunchentootのレスポンスは0.4秒と速いのですが、同時接続が20越えると黙りこむことがあるようです。同時接続が20なんてまあまあよくあることなので、このままでは今後継続して運用し切れないと判断して、別のバックエンドに移行することにしました。

そういう経緯もあり、今月ClackはFastCGI対応しました。これを使ってアプリケーションサーバFastCGIで動くようにし、ついでにフロントサーバもApacheからNginxに移行しました。これでレスポンスタイムは同じくらい高速で、同時接続数は100以上でも安定して稼働できそうです (社内限定リリースなのでまだそこまでの負荷は来ていませんよ)。FastCGIなのでstreamingも可能です。

メモリ使用量


アプリケーションサーバを1週間くらい動かしていると、メモリ使用量がどんどん上がっていきます。SBCLで、何がメモリを食っているかを調べたいときは以下の関数が有効です。

(sb-vm:instance-usage :dynamic :top-n 10)

まあ、512MBのサーバの一角で動かすようなものでもないんですけどね……。

まとめ


最初、会社の開発合宿でCommon Lispのアプリケーションを書くことに、僕も悩みはしたのですが、こういう場では作ってて楽しいものでなければ後悔する、と思ってCommon Lispを選びました。

実際作ってみると、アプリケーションはかなり高速で、だいたい同時接続数が増えても0.5秒程度で返ってくるし、ローカルのEmacsから本番に反映して試す、ということが容易にできるため、問題が発見しやすく、開発のスピードも保つことができました。これは本当です。

開発から社内リリースを経てしばらく運用して思ったのは、SQLite3からMySQLに、もしくはHunchentootからFastCGIに、移行したいと思ったときに容易に移行できることは当然のごとく重要だなぁと思いました。

Hunchentootに依存しているフレームワークはWeblocksとかRESTASとか web4rとかいっぱいありますが、将来Hunchentootではパフォーマンス上問題があると思ったときに移行できないとするとかなり心配です。そういう実体験もあり、今回はClackやCL-DBIの重要性を再度実感しました (宣伝)。

興味がある方はぜひ使ってみてください。

こういうCommon Lispでの運用の話って表に出てこないことが多いので、今後も少しずつ増やしていきたいですね。

2012-02-28

Common LispのProperty Listユーティリティ「Multival-Plist」を公開しました

Clackではライブラリ内で使うユーティリティをsrc/util以下に置いているのですが、その1つであるgetf-allを独立ライブラリの「Multival-Plist」として公開しました。

通常、Property List(属性リスト)へのアクセスにはgetfを使いますが、リストに複数のキーが含まれている場合は最初に見つかったものを1つしか返してくれません。getf-allを使えば、マッチするキーの値をまとめてリストにして返してくれます。

(defparameter *plist* '(:foo 1 :bar 2 :foo 3))

(getf *plist* :foo)
;=> 1
(getf-all *plist* :foo)
;=> (1 3)

(remf-all *plist* :foo)
;=> T
(getf-all *plist* :foo :undef)
;=> :UNDEF

こういうのはAlexandriaにあってもよさそう、と作ったあとに思いました。

利用シーンがなかなか思い浮かばないかもしれないですが、たとえばClackではClack.Requestでのクエリ文字列へのアクセスに使っています。チェックボックスを含むフォームを送信したとき、クエリ文字列に「name=fukamachi&name=m2ym」のように複数のキーを持つものを受け取ることがあります。getf-allはこのようなものもProperty Listとして自然に扱えるため便利なわけです。

Circular-Streamsに続いてこちらも小さなリリースですが、興味があればお使いください。