Kahuaプログラミング入門

〜継続渡しスタイルによる動的Webプログラミング〜

備前 達矢(び)

Kahuaプロジェクト

まずは自己紹介

  • 現在はKahuaプロジェクトのメインプログラマ
  • 本格的に参加したのは昨年5月から
  • Kahuaの創成期のいきさつはよく知らない
  • 最初は単なるユーザだった
  • 現在はKahuaの基盤部分を強化中

Kahuaって何よ

全く予備知識なしの人はいないかもしれないけど念のため:

  • Gaucheで書かれたアプリケーションフレームワーク/実行環境→ 実はWebアプリ専用なわけではない
  • オープンソースソフトウェア(修正BSDライセンス)
  • Scheme(Gauche)でアプリケーションコードを記述
  • S式(特にSXML)を基本データ表現形式として使用
  • 継続ベース: 継続渡しを多用するプログラミングスタイル
  • オブジェクトデータベース: オブジェクトの永続化が簡単

激しく進化中なので将来変わるかもしれない。

Kahuaを準備する(1)

準備するもの:

  • Gauche 0.8.10 (最新のリリース版)
  • autoconf 2.59以降(アプリのビルドに必要
  • make (同上/GNUでもBSDでもAppleのでも)
  • Kahua 1.0.3 (最新の安定版)
  • エディタ(Emacs推奨/もちろんviや他のエディタでもOK)

Kahuaを準備する(2)

注意:

  • GaucheはPOSIXスレッドサポートが必要
     % ./configure ... --enable-threads=pthreads ...
    
  • LinuxはNPTLが必要→カーネル2.6以降推奨
    % getconf GNU_LIBPTHREAD_VERSION
    NPTL 2.4
    
  • 内部文字エンコーディングとしてはUTF-8を推奨
    % ./configure ... --enable-multibyte=utf-8 ...
    

Kahuaを準備する(3)

こうなってればOK

% gosh -V
Gauche scheme interpreter, version 0.8.10 [utf-8,pthreads]

Kahuaのインストール(1)

詳細な手順については、Kahuaのサイトの「チュートリアル Step0」を参照してもらうとして...

とりあえず /usr/local/kahua の下にまとめて放り込むのがお薦め。

Kahuaのインストール(2)

  • /usr/local/kahuaの下にまとめる:
    % ./configure --prefix=/usr/local/kahua
    % make
    % make -s check
    % sudo make install
    
  • $HOME/kahua の下にまとめて放り込む:
    % ./configure --prefix=$HOME/kahua
    % make
    % make -s check
    % make install
    

Kahuaアプリケーション開発の段取り

  • 試験動作用のサイトバンドル(後述)を作成
  • アプリケーションのスケルトンを生成
  • とりあえずサイトバンドルにインストールして動作確認
  • あとはひたすらコードを書く/実行するの繰り返し。
    • 式をひとつ書いたらkahua-shellで評価
    • ある程度まとまったら
      % make install
      % kahua-admin update ワーカ名
      

サイトバンドルとは

  • Kahuaのアプリケーションコードを始め、静的コンテンツファイルやデータベース、設定ファイルなど、サイトを構成する要素をひとつのディレクトリ構造に収めたもの
  • Zopeのインスタンスディレクトリや、Railsアプリケーションのディレクトリ構造に似てるかも(実はよく知らない)
  • ヒントにしたのはNeXT(古っ)やMac OS Xのアプリケーションバンドル
  • サイト全体を手軽にバックアップしたり持ち運んだりしたかった

サイトバンドルの作成

$HOME/workの下に、siteという名前でサイトバンドルを作成する。

% kahua-package create ~/work/site

注意:

  • スケルトン生成のkahua-package generateとまぎらわしい
  • サイトバンドルを作り忘れてもアプリのインストールができてしまう

サイトバンドルの中身

% cd ~/work
% find site
site
site/app
site/app-servers
site/database
site/etc
site/etc/kahua.conf
site/etc/user.conf
site/logs
site/plugins
site/run
site/socket
site/static
site/templates
site/tmp

サイトバンドルを指定する

  1. コマンドラインオプションで -S '/path/to/site' オプションを指定する
  2. 環境変数KAHUA_DEFAULT_SITEを設定する

Kahuaスーパバイザを起動する

Kahuaを起動するとは、kahua-spvr(スーパバイザ)を起動すること。kahua-spvrが残りのサーバプログラム群を起動/管理する。

% kahua-spvr -S ~/work/site -H 8088

コマンドラインオプション:

  • -S '/path/to/site' : サイトバンドルのパスを指定
  • -H [host:]portnum : 組み込みHTTPd(kahua-httpd)を使う時、listen(2)するアドレスやポートを指定

kahua-spvrが起動するサーバ群

  • kahua-server:
    実際にアプリケーションコードを読み込んで実行する。ワーカ/ワーカプロセスとも呼ぶ
  • kahua-keyserv:
    セッション情報やコンテキスト情報を一元管理する
  • kahua-httpd:
    HTTPd/開発中はこれをWebサーバとして使用することが多い。

kahua-spvrの起動方法あれこれ

  • kahua-spvrは自らdaemonizeする機能を持たない
    • &をつけてバックグラウンドで起動する
      % (kahua-spvr -S ~/work/site -H 8088 >>/path/to/stderr.log 2>&1 &)
      
    • djb(Dan J. Bernstein)daemontoolsのsupervise配下で動かす
      本番運用時はこちらがお薦め
  • 開発中は、 #?= の出力を標準エラー出力に垂れ流すことも多い
    • フォアグラウンドで普通に起動しておく
    • GNU screenを使って以下のように起動し、必要なときだけattachする(お薦め)
      % screen -d -m kahua-spvr -S ~/work/site -H 8088
      

さてお題は?

→ redditもどきを作る

さてお題は?

(またブックマークかよ)

なぜredditもどき?

  • 第3回Kahuaセミナーのリベンジ
  • Gauche の作者 shiro さんが、4月頃にKahuaでredditもどきを書いて自らのWiLiKiで公開
  • でも、ちょっとスタイルが古い
  • Kahua開発者としては新しい機能を使って欲しい
  • じゃあ書き直してみよう

スケルトンを生成する

まずは$HOME/workの下に bookmarks というスケルトンを作成する。

% kahua-package generate bookmarks
Creator Name> Tatsuya BIZENN
E-Mail Address> bizenn@kahua.org
% cd bookmarks

スケルトンをビルド/インストールする(1)

生成されたそのままをビルドして先ほどのサイトバンドル(~/work/site)にインストールしてみる。

% ./DIST gen
% ./configure --prefix=/usr/local/kahua \
        --with-site-bundle=$HOME/work/site
        :
% make
        :
% make check
        :
% make install

スケルトンをビルド/インストールする(2)

configureオプションの意味:

  • --prefix='Kahuaのconfigureに渡したprefixの値'
  • --with-site-bundle='インストール先のサイトバンドル'

アプリケーションを起動する(1)

$HOME/work/site/app-serversファイル(起動設定ファイル)に

(bookmarks :arguments () :run-by-default 1)

を追加する。

;; -*-scheme-*-
;; Application Service Configuration alist.
;;
(;;Each entry follow this format:
 ;;(<type> :arguments (<arg> ...) :run-by-default <num>
 ;;        :profile <path-to-profile-base>
 ;;        :default-database-name <path-to-database>)
 (bookmarks :arguments () :run-by-default 1))

アプリケーションを起動する(2)

kahua-adminコマンドを起動してapp-serversを読み直させる。

% kahua-admin -S ~/work/site 
spvr> ls
wno   pid type         since        wid
spvr> reload
(bookmarks)
spvr> ls
wno   pid type         since        wid
  0 23223 bookmarks    May 23 17:57 hx3:dudm

reloadコマンドは、app-serversを読み直し、必要ならワーカを起動する。すでに起動しているワーカプロセスに対しては何もしない。

開発ユーザを追加する

ついでに後ほどkahua-shellで使用する開発ユーザをサイトバンドルに追加しておく。

spvr> lsuser
()
spvr> adduser bizenn hogehoge
done
spvr> lsuser
("bizenn")

Emacs環境の整備

  • 統合環境は存在しない。イマドキならEclipseのプラグインくらい用意しておくのが当たり前?
  • EmacsはLisper/Schemerのお友達
  • Kahuaに含まれているkahua.elの機能をうまく活かすと、開発効率がぐっと上がる(はず)
  • vi(m)や他のエディタをお使いの皆様、ごめんなさい

kahua.elのインストール

  • kahua.elをEmacsのload-pathのどこかにコピーする
  • .emacsに以下のフォームを追加する
    (require 'kahua)
    (setq auto-mode-alist
          (append '(("\\.kahua$" . kahua-mode))
          auto-mode-alist))
    

kahua.elの使い方

  • M-x run-kahua でkahua-adminコマンドをバッファ内で起動する
  • M-x run-kahua-shell でkahua-shellコマンドをバッファ内で起動する
  • Kahuaモードのバッファ内で、C-xC-eやC-cC-eを打鍵すると対話的に式をkahua-shell経由で評価することができる

実はEmacs 21.4ではこの機能が動かないらしい... 確認して、1.0.4までには直します。

アプリケーションコードがやること

*.kahuaなファイルの役割は、継続エントリ永続クラスを定義すること

  • 継続エントリは、URIにマッピングされ、クライアントからのリクエストで起動できる手続き
  • 永続クラスとは、そのインスタンスが暗黙のうちにKahuaオブジェクトデータベースに保存されるクラス

Kahuaのリクエストレスポンスサイクル

  • kahua-spvr(スーパバイザ)はリクエストを受け取り、そのURIからリクエストを適切なワーカに転送する
  • ワーカは、リクエストを構成するURIやクエリパラメータなどから、適切な形で定義されている継続エントリを呼び出す
  • 継続エントリはその定義に従って処理を行い、処理の最後にレスポンスを表現したS式(SXML)もしくは高階タグ(後述)をワーカに返す
  • ワーカはリクエスト処理の最後に永続クラスのインスタンスに対する生成や変更をKahuaオブジェクトデータベースに反映する
  • 継続エントリの返したS式から適切なレスポンスを組み立ててクライアントに返す

redditもどきを構成するもの

  • ブックマークを保持する永続クラス
  • ブックマーク一覧を表示する継続エントリ
  • URL登録画面を表示する継続エントリ
  • URLへのリンクをクリックしたらカウントアップする継続エントリ
  • スコアを上げ(up)下げ(down)する継続エントリ

ブックマークエントリクラスの定義

ブックマークエントリを保存するためのクラスを定義する。

(define-class <bookmark-entry> (<kahua-persistent-base>)
  ((url :init-keyword :url :allocation :persistent
        :index :unique)  ;; 重複を許さない
   (title :init-keyword :title :allocation :persistent
          :index :any)   ;; 重複を許す
   (score :init-value 1 :allocation :persistent)
   (count :init-value 0 :allocation :persistent)))

このクラスのインスタンスは暗黙のうちにオブジェクトデータベースに保存される。

永続クラス

  • <kahua-persistent-base>を継承したクラス
  • このクラスのインスタンスは自動的にKahuaオブジェクトデータベースに保存、反映される
  • スロットオプション :allocation :persistent をつけたスロット値が保存される
  • スロットオプション :index :unique をつけたスロットの値は、インスタンス間でユニークであることが保証される
  • スロットオプション :index :any をつけたスロットの値は、インスタンス間で重複が許される
  • :indexのついたスロット値でインスタンスを検索できる

kahua-shellから評価してみる

  • Emacs内で M-x run-kahua-shell を使ってkahua-shellを起動
  • クラス定義の式を評価
  • インスタンスを作ってみる
  • 作ったインスタンスを抽出してみる

ブックマークの一覧表示(継続エントリ)

(define page-template
  (kahua:make-xml-template
   (kahua-template-path "bookmarks/page.xml")))
(define-entry (index)
  (define (bookmark-list/)
    (table/
     (map/ (lambda (bm)
             (tr/ (th/ (a/ (@/ (href (ref bm 'url)))
                           (ref bm 'title)))
                  (td/ (ref bm 'count))
                  (td/ (ref bm 'score))))
           (coerce-to
            <list> (make-kahua-collection <bookmark-entry>)))))
  (kahua:xml-template->sxml
   page-template
   :title (title/ "Reddit modoki revised")
   :body (bookmark-list/)))

define-entry (1)

名前つきの継続エントリ(有名エントリ/well knownエントリ)を定義する。

書式:

(define-entry (entry-name path1 path2 ...
                   :keyword param1 param2 ...
                   :mvkeyword mvparam1 mvparam2 ...
                   :rest restpaths)
  body ...)

define-entry (2)

次のようなHTTPリクエストがワーカに渡ってきたら、

POST /bookmarks/entry-name/fuga/hoge/moke/fugu HTTP/1.1

param1=value1&mvparam1=mvalue1

ワーカは以下のように変数が束縛された環境でbody部分を評価する(==継続エントリを起動する)。

path1 = "fuga"
path2 = "hoge"
param1 = "value1"
param2 = #f
mvparam1 = ("mvalue1")
mvparam2 = ()
restpaths = ("moke" "fugu")

高階タグ関数

  • table/, tr/, td/, a/ ...
  • 属性ノードとして @/ を、補助属性ノードとして @@/ を使用する
  • SXMLのノード(XMLの要素)を表現する関数(高階タグ)を返す関数
  • 引数を中身とする要素を返す関数だと見なしてよい
  • map/ はmapに似た特殊な高階タグ関数: mapした結果の要素群をまとめた仮想的な要素を返す
  • 素のS式より組み立てやすい
  • 継続エントリから高階タグが返されると、ワーカがそれを暗黙のうちにレスポンスを表すSXMLに変換する

ページテンプレート

  • XMLやXHTMLを読み込んでページテンプレートに変換(kahua:make-xml-template)
  • テンプレートをSXMLに変換しつつ、与えられたキーワード引数に従って要素を置換していく
    (kahua:xml-template->sxml)
  • 将来はもうちょっと複雑なこともできるようにするかも

URL登録画面 (1/2)(継続エントリ)

(define-entry (new)
  (define (submit-url-form/)
    (form/cont/
     (@@/ (cont (entry-lambda (:keyword url title)
                  (make <bookmark-entry>
                    :title title :url url)
                  (redirect/cont (cont index)))))
     (table/
      (tr/ (th/ "Title: ")
           (td/ (input/ (@/ (name "url")))))
      (tr/ (th/ "URL: ")
           (td/ (input/ (@/ (name "title")))))
      (tr/ (th/)
           (td/ (input/ (@/ (type "submit")
                            (value "Submit"))))))))
  ;; 次のページに続く

URL等録画面 (2/2)(継続エントリ)

  ;; 前のページから続く
  (kahua:xml-template->sxml
   page-template
   :title (title/ "Submit URL")
   :body (submit-url-form/)))

form/cont/

  • form/ と同様に form 要素を生成する高階タグ関数
  • (@@/ (cont entry-name ...)) と書くことで、actionとしてURLを指定する代わりに、継続エントリを指定する
  • @/(属性) ではなくて @@/(補助属性) (まぎらわしい)
  • 無名エントリも指定できる

entry-lambda

無名の継続エントリを定義する

(entry-lambda (path1 path2 ...
                    :keyword param1 param2 ...
                    :mvkeyword mvparam1 mvparam2 ...
                    :rest)
  body ...)

引数の意味やURLの各要素とのマッピングはdefine-entryと同じ

(define-entry (entry-name ...) ...)
==
(define-entry entry-name (entry-lambda (...) ...))

継続エントリの正体

  • Schemeから見ると単なる引数を持たない手続き(thunk)
  • entry-lambda(マクロ)がリクエストコンテキストからパス要素やクエリパラメータなどを取り出してローカル変数に束縛するコードに展開している
  • define-entry(マクロ)が継続エントリに、外からパーマネントURLとして呼び出すための名前をつける
  • 従って、パス要素やクエリパラメータを受け取る必要がないならただのthunkを継続エントリとして使える
  • define-entryやentry-lambdaで定義した手続きをただのthunkとして呼び出すこともできる

redirect/cont

  • 継続エントリにリダイレクトするSXMLを生成するマクロ
  • 継続エントリの最後に呼び出す(呼んだらすぐリダイレクトされるわけではない)
  • 書式:
    (redirect/cont (cont entry-name ...))
    
  • 最後に/がつかないこと(まぎらわしい!!)、引数を(@@/ )で囲む必要がないこと(まぎらわしい!!!)に注意

参照回数のカウント (1)

(define (go/countup bm)
  (inc! (ref bm 'count))
  (html/
   (extra-header/ (@/ (name "Status") (value "302 Found")))
   (extra-header/ (@/ (name "Location") (value (ref bm 'url))))))
;; 次のページに続く

参照回数のカウント (2)

;; 前のページから続く
(define-entry (index)
  (define (bookmark-list/)
    (table/
     (map/ (lambda (bm)
             (tr/ (th/ (a/cont/
                        (@@/ (cont (cut go/countup bm)))
                        (ref bm 'title)))
                  (td/ (ref bm 'count))
                  (td/ (ref bm 'score))))
           (coerce-to
            <list> (make-kahua-collection <bookmark-entry>)))))
  (kahua:xml-template->sxml
   page-template
   :title (title/ "Reddit modoki revised")
   :body (bookmark-list/)))

extra-header/

  • レスポンスヘッダを追加/変更するための特殊な高階タグを生成する関数
  • ルート要素にはなれないので、何らかの要素の子要素にする必要がある。ここではhtml/ の下に入れている
  • 実は現在のKahuaには、特定のURLにリダイレクトするための手続きが定義されていない(!)ためにこういう小細工が必要になる

a/cont/

  • a/ 同様、a要素を返す高階タグ関数
  • form/cont/ 同様、(@@/ (cont entry-name ...)) として、hrefにURLを指定する代わりに継続エントリを指定する
  • @/(属性)ではなく@@/(補助属性) (まぎらわしい)
  • もちろん、無名エントリも指定できる

スコアの上げ下げと仕上げ

キモはここだけ

(td/ (a/cont/ "[up]"
              (@@/ (cont (lambda ()
                           (inc! (ref bm 'score))
                           (redirect/cont (cont index))))))
     (a/cont/ "[down]"
              (@@/ (cont (lambda ()
                           (dec! (ref bm 'score))
                           (redirect/cont (cont index)))))))

できあがり

shiroさんのWiLiKiの Kahua:Reddit-modoki のページに貼ってあります。また、パッケージとして、www.kahua.orgからもダウンロードできるようにしておきます。

残された課題

  • 検索機能はきっと欲しくなる
  • カテゴリは? やはりイマドキならタグか
  • バリデーションくらいはちゃんとやろう
  • 同じURLを登録しようとした場合はエラーじゃなくスコアupとして扱うべき

どれもそう難しくないし、チュートリアルでは一部触れているのでやってみて下さい。

Kahuaの今後

  • Kahuaオブジェクトデータベースの刷新
  • サーバ間通信の効率化
  • スケールアウトするための仕組み
  • より親切なAPIとドキュメント
  • さまざまなTipsやノウハウを整理して公開

ご清聴ありがとうございました。
今後もKahuaをよろしくお願いします。