20分ではてなブログを作る方法
これはLisp Advent Calendarの10日目です。
先日、HappyElementsさんで行われたLiveCoding #11にライブコーダとして参加させていただきました。そのときの話をしようと思います。
LiveCodingとは
一人の"LiveCoder"が、20分の制限時間で何かソフトウェアを作り、デモをします。 LiveCoderがどのようにコーディングしているかの全てがプロジェクターで大画面に映し出され、 さらに解説役が常にLiveCoderの挙動を説明します。
A 「コンパイル中です・・・、お、おおお、なんと、Syntax error!」
B 「これは恥ずかしい…!」
LiveCoder 「(/// 」
LiveCoderが何を作るかについては、自分で決めた上でそれをLiveCoding前に告知します。 ただし、観客の要望で仕様が強制的に変更になることがあります。 もっとも、客観的に20分以内に実装できないことが明らかな場合、仕様変更要望は無視できます。
一人のLiveCoderに対して二人の解説役が付くことがうまくいくという経験則があります。
LiveCoderは過去に作ったものと全く同じものをLiveCodingで作ることが禁止されています。 もはやそれはLiveCodingではなく、たんなる動画の再生と見なされます。
参加のきっかけはILCでお会いしたyharaさんに勧められたことだ。
「Common Lispでいいですか」
「はい、むしろCommon Lispでお願いします。」
Common Lispでいいならなんとかなるだろう。
作るもの
今回、Common Lispで何かを作るということだけはあらかじめ決まっていたものの、何を作るかは未定だった。
何か風変わりなものを作ろうとも考えたが、Lispに詳しい方なんてほとんど来ないので、むしろ普通のものを作ってみては、という助言をいただいたので、普通にWebサービスを作ることにした。
Webサービスなら自分のホームグラウンドでもある。20分で現実的に作れるもので、誰もが知っているようなサービス──ブログ。20分でブログを作ることに決めた。
方針
持ち時間は20分と非常に短い。Common Lispがいかに高速な開発を可能にするとしても、20分で完璧なものを作るのは無理だ。いくらか手を抜かなければならない。
とりあえず動くものを作るのが最優先だろう。パフォーマンスは気にしない。セキュリティも、まあいいだろう。CSSなんて書いている暇はない。とにかく短いコード、短時間で動くものを作る、そのために必要なことだけに注力することにした。
1. プロジェクト作成
まずはプロジェクトを作る。Common Lispだとプロジェクトを作らずにいきなりREPLで始めることもできるが、途中でEmacsがクラッシュしない保障はない。プロジェクトはファイルに書き出しておくのが賢明だ。
(ql:quickload :cl-project) (cl-project:make-project #P"~/Programs/web/hatenablog/" :description "Write the story of your life.")
2. サーバ起動
さて、ブログに最も必要な機能はなんだろう?
ブログなのでWebブラウザ上で見られないと使ってもらえない。まずはサーバを起動してみよう。
今回はインタラクティブで高速な開発が必要なため、Webフレームワークとしてningleを使うことにした。ningleより今回の目的に合ったフレームワークはないだろう。
(ql:quickload :ningle) (defvar *app* (make-instance 'ningle:<app>)) (clack:clackup *app*) ;-> Hunchentoot server is started. ; Listening on localhost:5000. ;=> #<CLACK.HANDLER:<HANDLER> #x3020061A081D>
これでHunchentootをバックエンドとしてブラウザ上で見られるようになった。⌘-Tabを押してChromiumに移動して http://localhost:5000 に移動する。
う、何も表示されてない……。
あっ。
思い当たることあって⌘-⌥-Iを押して開発ツールを開く。
404だった。何もルーティングルール書いてないんだからそりゃそうだ。とりあえず動いてはいるようなので安心した。
3. ルーティング設定
トップページが404のWebサービスというのは斬新だが、早々に解決しなければならない。
ルーティングルールを書いて小粋な挨拶をさせてみよう。
(setf (ningle:route *app* "/") #'(lambda (params) "Hello, I'm Hatena Blog."))
ブラウザに戻ってリロード。
順調。
4. データ定義
このブログサービスはまだ生まれたばかりなので、ブログを書くことができない。これでは誰にも使ってもらえない。次はエントリを書けるようにしてみよう。
Common Lispは完全なオブジェクト指向言語なので、まずはクラス定義をする。
(defclass <entry> () ((title :type 'string :initarg :title :initform "") (body :type 'string :initarg :body :initform "")))
とりあえずタイトルと本文があれば十分だろう。エントリを作るための関数も用意しておく。
(defvar *entries* nil) (defun create-entry (&key title body) (pushnew (make-instance '<entry> :title title :body body) *entries*))
とにかく僕には時間がない。既に10分が経過しようとしている。残り10分ほどで動くものにしなければならない。DBスキーマを作ってエントリをDBに入れている暇なんてない。すべてオンメモリだ! 生まれて10分と少しのWebサービスということで甘んじることにする。
(create-entry :title "Hi" :body "This is my first blog entry. Hehe.") ;=> (#<<ENTRY> #x3020061CC9CD>)
これでエントリを作れた。よし。
5. ブラウザに表示
次はエントリをブラウザに表示させよう。
新しく /entries というページを作って、そこで時系列でエントリ一覧を見られるようにする。
テンプレートエンジンにはCL-EMBを使うことにした。これはHTMLの中に独自構文で変数を展開したりループできたりするよくあるテンプレートエンジンだ。複雑なHTMLを吐くのは面倒だが、ただ表示するだけなら十分だろう。
templatesディレクトリ以下に entries.tmpl というファイルを作っておく。
<html xmlns="http://www.w3.org/1999/xhtml" lang="ja" xml:lang="ja"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>All your blog entries</title> </head> <body> <% @loop entries %> <h3><% @var title %></h3> <p><% @var body %></p> <% @endloop %> </body> </html>
あとは entries という名前でエントリのリストを渡してあげればいい。
(ql:quickload :cl-emb) (setf (ningle:route *app* "/entries") #'(lambda (params) (emb:execute-emb #P"entries.html" :env (list :entries (loop for entry in *entries* collect `(:title ,(slot-value entry 'title) :body ,(slot-value entry 'body)))))))
少し冗長。とはいえこれでエントリ一覧が見れるはずだ。
できてる。1個だとわかりづらいからもう1個くらいエントリを作っておこう。⌘-TabでEmacsに戻り、REPLでcreate-entryを打ち込む。
(create-entry :title "Yippee!" :body "Hatena Blog is already working!")
Chromiumに戻ってリロード。
都合がいいことにpushnewすると新しいものが上に来るようになっている。素晴らしい。
6. エントリを書くフォーム
これでREPLからエントリを書けるというLisperフレンドリなブログシステムが完成した。思いついたときに即座にREPLを立ちあげ、create-entry。クール。
しかし、ユーザ様の中にはCommon Lispをインストールしていない方もおられるかもしれない。ブラウザ上でもエントリを書けるに越したことはないだろう。
この時点で残りは5分もない。行けるか……。完成が危ういが、このまま5分間観客と談話して過ごすつもりもない。仕方ない。やってみよう。
/post というページを新しく作って投稿フォームを表示することにする。
今回もCL-EMBでやろうと思ったが、悠長にしてられない。CL-Markupで少し楽をする。
試しにREPLでmarkupマクロを呼び出して、ちゃんと求めるHTMLが返ってくるか確かめてみる。
(ql:quickload :cl-markup) (markup:markup (:form :method "POST" (:input :type "text" :name "title")(:br) (:textarea :name "body" nil))) ;=> "<form method=\"POST\"><input type=\"text\" name=\"title\" /><br /><textarea name=\"body\"></textarea></form>"
良さそうだ。タイトルと本文を入力するだけのシンプルなHTML。
これをルーティングルールに組み込んでやれば良い。HTML全体を出力するにはmarkupマクロをxhtmlマクロに変更すればいい。
(setf (ningle:route *app* "/post") #'(lambda (params) (markup:xhtml (:form :method "POST" (:input :type "text" :name "title")(:br) (:textarea :name "body" nil)))))
あまりに簡単過ぎて恐ろしくなってくるが、Chromiumに戻ってリロード
しまった。submitボタンを忘れてた。これでは投稿できない。
急いでinputタグを追加して再評価する。
;; 再評価 (setf (ningle:route *app* "/post") #'(lambda (params) (markup:xhtml (:form :method "POST" (:input :type "text" :name "title")(:br) (:textarea :name "body" nil)(:br) (:input :type "submit" :value "投稿!")))))
投稿フォームが完成。でもまだPOST先を作れてない。投稿ボタンを押すと404。残り時間はあと2分。急いだほうがいい。
POSTのときに反応するルーティングは :method :post を追加するだけでいい。
(setf (ningle:route *app* "/post" :method :post) (lambda (params) (let ((title (getf params :|title|)) (body (getf params :|body|))) (create-entry :title title :body body) "投稿成功!")))
POSTされたら単純にcreate-entryする。これでいけるはず。アドレナリンが噴出する。30秒のコールはすでに過ぎていたが、まだ時間はあるか。
ブラウザから投稿を試す。
きた! 果たしてちゃんとエントリが作られたか。
⌘-Lを押してロケーションバーに素早くカーソルを合わせ、/entries を開く。
一番上に表示された! 残り時間は ―― 4秒。Common Lispの勝利である。
おわりに
ご覧のように、Common Lispは高速なプロトタイピングが可能な言語です。REPLで一発でサーバを起動し、インタラクティブにルーティングルールを定義、再定義、確認のサイクルが回せるのが強みです。
他の言語でもREPLと呼ばれているようなものはあるかもしれませんが、そのプロセスを環境として試しながら組み立てるみたいな使い方はされていないようですし、ライブラリ側もそういった使い方を考慮して設計されていません。インタラクティブ性はLispが持つ強みの1つです。
もちろん、今回のプロトタイピングには出て来なかった、解決すべき問題も多くあります。
たとえばDBアクセス。今回はすべてオンメモリで済ませることにしましたが、実運用までにDBに入れなければいけません。ただ、その場合もPostmodernやElephantのようなObjectStoreを使えばクラス定義をそのまま使え、プロトタイピングからあまりコードを書き換えずに永続化できるはずです。
高速なプロトタイピング、そして最速なリリースを目指すならCommon Lispが最良の選択です。
ちなみにはてなブログは以下のリンクから開設することができます。どうぞご利用ください。
※これは実話を元にしたフィクションです。はてなブログはCommon Lisp以外の言語で動いています。