2011年6月30日木曜日

Emacsのコマンド

貴重なC-a,C-eといったキーが行頭/行末移動しかしないのはもったいない、ということで、 sequential-command.elなどのように、多少空気を読んで動作を変えるようなコマンドを定義します。

(defmacro as-this-command (cmd &rest args)
`(progn
(setq this-command ',cmd)
(call-interactively ',cmd ,@args)))

(defun buffer-empty? ()
(= (point-min) (point-max)))

(defun initialize-buffer ()
(interactive)
(call-interactively 'auto-insert))

(defun at-line-start? ()
(= (point) (line-beginning-position)))

(defun at-line-end? ()
(= (point) (line-end-position)))

(defun at-word? ()
(case (char-after (point))
((9 10 13 32 59
?( ?) ?[ ?] ?{ ?}) nil)
((nil) nil)
(t t)))

(defun at-paren-start? ()
(find (char-after (point))
"([{"))

(defun at-paren-end? ()
(when (> (point) 1)
(find (char-after (point))
")]}")))

(defun at-end-of-symbol? ()
(when (> (point) 1)
(save-excursion
(unless (at-word?)
(backward-char)
(at-word?)))))

(defun forward-at-paren-end ()
(interactive)
(let ((pos (point)))
(cond
((= pos (point-max)) (call-interactively 'newline-and-indent))
((= pos (line-end-position))
(call-interactively 'forward-char)
(call-interactively 'indent-for-tab-command))
(t (call-interactively 'forward-char)))))

(defun forward-at-line-end ()
(interactive)
(let ((pos (point)))
(cond
((= pos (point-max)) (call-interactively 'newline-and-indent))
(t
(call-interactively 'forward-char)
(call-interactively 'indent-for-tab-command)))))

(defun my-ctrl-o ()
(interactive)
(cond
;; markが有効な場合、インデントする
(mark-active (as-this-command indent-region))
;; バッファが空の場合、初期化する(auto-insert)
((buffer-empty?) (as-this-command initialize-buffer))
;; ポイントが単語上にある場合、次の単語に移動する
((at-word?) (as-this-command forward-word))
;; ポイントが開き括弧上にある場合、対応する括弧の終わりに移動する
((at-paren-start?) (as-this-command forward-sexp))
;; ポイントが単語の終わりにある場合、hippie-expandを呼び出す
((at-end-of-symbol?) (as-this-command hippie-expand))
;; ポイントが閉じ括弧の次にある場合、次の文字に進む
((at-paren-end?) (as-this-command forward-at-paren-end))
;; ポイントが行頭にある場合、インデントする
((at-line-start?) (as-this-command indent-for-tab-command))
;; ポイントが行末にある場合、次の行に移動する。
;; バッファの終端でもある場合、改行する。
((at-line-end?) (as-this-command forward-at-line-end))
(t t)))

(defun my-ctrl-a ()
(interactive)
(when (and (eq last-command 'my-ctrl-a)
(= (point) (line-beginning-position)))
(call-interactively 'scroll-down))
(call-interactively 'move-beginning-of-line))

(defun my-ctrl-e ()
(interactive)
(when (and (eq last-command 'my-ctrl-e)
(= (point) (line-end-position)))
(call-interactively 'scroll-up))
(call-interactively 'move-end-of-line))

(global-set-key (kbd "C-o") 'my-ctrl-o)
(global-set-key (kbd "C-a") 'my-ctrl-a)
(global-set-key (kbd "C-e") 'my-ctrl-e)

my-ctrl-aコマンドはC-aに割り当てるつもりで定義したものです。普段は通常のC-aの動作をしますが、すでにポイントが行頭にあり、 1つ前に実行されたコマンドもmy-ctrl-aの場合には、M-v(scroll-down)の動作を行います。

my-ctrl-eコマンドはmy-ctrl-aのC-eバージョンです。 1つ前のコマンドがmy-ctrl-eの場合にはC-v(scroll-up)の動作を行います。

これでscroll-downが押しやすくなった上、C-vを他のコマンドに割り当てる余裕ができました。

また、普段はC-oをhippie-expandコマンドにしているので、展開が必要なさそうな箇所では別の動作をするmy-ctrl-oも定義しました。こちらはC-a/C-eに比べて残念な感じがします。

2011年6月15日水曜日

cl-gtk2 + Glade

cl-gtk2はgladeで作成したファイルを利用できるようなので遊んで見ました。

ソースコード

(asdf:load-system :cl-gtk2-glib)
(asdf:load-system :cl-gtk2-gdk)
(asdf:load-system :cl-gtk2-cairo)
(asdf:load-system :closure-html)
(asdf:load-system :cxml-stp)
(asdf:load-system :drakma)
(asdf:load-system :cl-ppcre)
(asdf:load-system :cl-interpol)

(defpackage :gtk-user
(:use :cl)
(:export run))

(in-package :gtk-user)

(cl-interpol:enable-interpol-syntax)
(setf drakma:*drakma-default-external-format* :utf-8)

(defun reference-of (node)
(let ((tag (stp:local-name node)))
(cond
((string= tag "a")
(or (stp:attribute-value node "href") ""))
((string= tag "img")
(or (stp:attribute-value node "src") ""))
((string= tag "link")
(or (stp:attribute-value node "href") ""))
((string= tag "script")
(or (stp:attribute-value node "src") ""))
(T ""))))

(defun text-of (node)
(let ((text (stp:string-value node)))
(if (> (length text) 30)
(concatenate 'string (subseq text 0 27 ) "...")
text)))

;; ありそうな文字コードを総当たりで試す。富豪的富豪的。
(defun octets-to-string-by-error-handler (octets)
(let ((formats (list :shift_jis :utf-8 :euc-jp :eucjp
:sjis
:utf-16 :utf-16BE :utf-16le
:utf-32 :utf-32be :utf-32le
:utf-8b )))
(tagbody
:retry
(print formats)
(handler-case
(return-from octets-to-string-by-error-handler
(sb-ext:octets-to-string octets :external-format (pop formats)))
(error (e)
(declare (ignore e))
(if formats
(go :retry)
(error "can't convert octets to string")))))))

(defun get-http-body-string (url)
(multiple-value-bind
(arr code headers url stream)
(drakma:http-request url :external-format-in :binary)
(let ((content-type (cdr (find :content-type headers :test #'string= :key #'car))))
(if content-type
(cl-ppcre:register-groups-bind (charset)
((cl-ppcre:create-scanner #?/charset=(\w+)/ :case-insensitive-mode t)
content-type)
;; todo
(octets-to-string-by-error-handler arr))))))

(defun run ()
(let ((out *standard-output*))
(gtk:within-main-loop
(let* ((builder
(make-instance 'gtk:builder
:from-file "/path/to/GladeTest.glade"))
(window (gtk:builder-get-object builder "ToplevelWindow"))
(entry (gtk:builder-get-object builder "entry1"))
(button (gtk:builder-get-object builder "button1"))
(tree (gtk:builder-get-object builder "treeview1"))
;; treeview1のmodelは後で上書きする
(dummy (gtk:builder-get-object builder "liststore1"))
(liststore (make-instance 'gtk:array-list-store)))

;; treeview1のmodelを上書き
(setf (gtk:tree-view-model tree) liststore)

;; tree-viewの列(model)
(gtk:store-add-column liststore "gchararray" #'stp:local-name)
(gtk:store-add-column liststore "gchararray" #'text-of)
(gtk:store-add-column liststore "gchararray" #'reference-of)

;; tree-viewの列(view)
(let ((col-tag (make-instance 'gtk:tree-view-column :title "タグ"))
(col-text (make-instance 'gtk:tree-view-column :title "text"))
(col-ref (make-instance 'gtk:tree-view-column :title "参照先"))
(cr (make-instance 'gtk:cell-renderer-text)))
(gtk:tree-view-column-pack-start col-tag cr)
(gtk:tree-view-column-add-attribute col-tag cr "text" 0)
(gtk:tree-view-column-pack-start col-text cr)
(gtk:tree-view-column-add-attribute col-text cr "text" 1)
(gtk:tree-view-column-pack-start col-ref cr)
(gtk:tree-view-column-add-attribute col-ref cr "text" 2)
(gtk:tree-view-append-column tree col-tag)
(gtk:tree-view-append-column tree col-text)
(gtk:tree-view-append-column tree col-ref))

;; ボタンクリック時の動作
(gobject:g-signal-connect
button "clicked"
(lambda (b)
(handler-case
(let* ((str (get-http-body-string (gtk:entry-text entry)))
(doc (chtml:parse str (cxml-stp:make-builder))))
(stp:do-recursively (node doc)
(when
(and
(typep node 'stp:element)
(some
(lambda (s) (string-equal s (stp:local-name node)))
'("a" "link" "script" "img")))
(gtk:store-add-item liststore node))))
(error (e)
(let ((diag (make-instance 'gtk:message-dialog
:text (format nil
"error:(~A) ~A"
(gtk:entry-text entry)
e)
:message-type :error)))
(unwind-protect (gtk:dialog-run diag)
(gtk:object-destroy diag)))))))
(gtk:widget-show window)))))


gladeファイル

<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="2.16"/>
<!-- interface-naming-policy toplevel-contextual -->
<object class="GtkWindow" id="ToplevelWindow">
<property name="title" translatable="yes">ToplevelWindow</property>
<child>
<object class="GtkVBox" id="vbox3">
<property name="visible">True</property>
<child>
<object class="GtkHBox" id="hbox1">
<property name="height_request">30</property>
<property name="visible">True</property>
<child>
<object class="GtkLabel" id="label5">
<property name="visible">True</property>
<property name="label" translatable="yes">URL:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="entry1">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">•</property>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="button1">
<property name="label" translatable="yes">読み込み</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkTreeView" id="treeview1">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">liststore1</property>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<object class="GtkListStore" id="liststore1"/>
</interface>

利用しているライブラリはすべてquicklispでインストールできます。

ライブラリのおおまかな内容は以下のとおり。

  • cl-gtk2-*** : GTKバインディング
  • closure-html : HTMLパーサー
  • cxml-stp : DOMに似たもの
  • cl-ppcre : 正規表現
  • drakma : HTTPクライアント
  • cl-interpol : リーダーの拡張。正規表現リテラルに利用。

sb-ext:octets-to-stringを利用しているのでSBCLでのみ動作します。他の処理系で動作ささせる場合、バイト列を文字列に変換する箇所を変更する必要があります。

(gtk-user::run) を評価するとテキストボックス(entry)を持ったウィンドウが表示されます。このテキストボックスにURLを入力して隣のボタンをクリックすると、URLの内容(HTML)を取得し、他のURLへを参照していそうなa/link/img/scriptタグを抜き出してtree-viewに表示します。

わかりづらかった点として、gtk:array-list-storeがCommonLisp側で定義されたクラスだということがありました。 array-list-storeは便利そうだと思いましたが、GTK側に組み込まれているクラスではないので、 Gladeでモデルに指定できない(ような気がします)。

2011年6月11日土曜日

ネタ言語 primefu*k

ネタ言語を実装しました。

;; 素数リストの作成
(defun make-prime-list (n)
(let ((arr (make-array n :initial-element 0 :element-type '(integer 0 1))))
(setf (aref arr 0) 1)
(setf (aref arr 1) 1)
(loop
:for i from 2 below n
:when (zerop (aref arr i))
:do (loop
:for j from (* i 2) below n by i
:do (setf (aref arr j) 1)))
(loop
:for i from 0 below n
:when (zerop (aref arr i))
:collect i)))

(defparameter
*primes*
(coerce (make-prime-list 100000) 'vector))

;; 素因数分解
(defun integer-factorization (n prime-vector)
(let ((result nil))
(loop
:for x across prime-vector
:until (= n 1)
:do
(loop
:while (zerop (mod n x))
:do (setf n (/ n x))
:sum 1 into acc
:finally (push (cons x acc) result)))
(nreverse result)))

;; 数値を引き数として受け取り、Common Lispプログラムを返す
(defun n->cl (n primes)
(let ((operators
(mapcar #'cdr (integer-factorization n primes))))
(let ((tags nil)
(result nil))
(dolist (op operators)
(case op
((0) ;; >: ptr++
(push `(incf ptr) result))
((1) ;; <: ptr--
(push `(decf ptr) result))
((2) ;; +: (*ptr)++
(push `(incf (aref memory ptr)) result))
((3) ;; -: (*ptr)--
(push `(decf (aref memory ptr)) result))
((4) ;; .: putchar(*ptr)
(push `(write-char (code-char (aref memory ptr)))
result))
((5) ;; ,: *ptr=getchar()
(push `(setf (aref memory ptr) (char-code (read-char)))
result))
((6) ;; [: while(*ptr){
(let ((from (gensym))
(to (gensym)))
(push (cons from to) tags)
(push
`(when (zerop (aref memory ptr))
(go ,to))
result)
(push from result)))
((7) ;; ]: }
(destructuring-bind (from . to) (pop tags)
(push
`(unless (zerop (aref memory ptr))
(go ,from))
result)
(push to result)))
(T (error "unexpected operator"))))
`(let ((memory (make-array 30000
:initial-element 0
:element-type '(unsigned-byte 8)))
(ptr 0))
(tagbody
,@(nreverse result))))))

(defvar *bf-char->op*
"><+-.<[]")

;; Brainfu*kプログラムを数値に変換
(defun bf->n (bf-string primes)
(let ((result 1))
(loop
:for ch across bf-string
:for p across primes
:do (setf result
(* result
(expt p (position ch *bf-char->op*)))))
result))

(defvar *helloworld-bf*
"+++++++++[>++++++++>+++++++++++>+++++<<<-]>.>++.+++++++..+++.>-.------------.<++++++++.--------.+++.------.--------.>+.")


(defun execute (n &optional (primes *primes*))
(eval (n->cl n primes)))

;; test
(execute (bf->n *helloworld-bf* *primes*))

名前のとおり、中身はBrainfu*kです。

入力となるソースコードは数値で、素因数分解によって命令列が作成されます。

例えば"Hello, world!"と出力するプログラムは以下の値です。 (10進数、25桁ごとに改行)

4502303465384972596608528
2944087262557401643378529
8625080698782925063959121
7719172880530482008491550
8597084354818091584552183
1319098036757291006692817
9301283601343001317452270
0376267461517920956544956
5153324663016284459064839
1911228540917237768596719
1754627268505767395202314
3109320783377732443055786
7415159648646742451459077
8603291732011810376948079
2004773113078228397846041
4399383865663430345291483
4231649128778405945758270
3488234290362476776792281
3785655092937989870582015
2313906809006976695583076
4202816254753054328113083
3788768481801880471791973
8660596785756596281519027
4112020595197735280186913
1838747769408930883235538
9738739557566706588749933
2671373192590205308149393
8139362300

2011年6月10日金曜日

いまさら Arc Challenge を考える

時代は継続らしいので、cl-contを利用してこんな感じで書いてみます。

(defvar top-page (load-html #p"top.html"))
(defvar link-page (load-html #p"link.html"))
(defvar result-page (load-html #p"result.html"))

(defapp arc (state)
(show top-page state)
(let ((text state.reply$text))
(show link-page state)
(show result-page state `(:text ,text))))

(start #'arc)

2011年6月9日木曜日

defclassとdefmethodをいじくる

特定のシンボルを特別扱いして、省略表記として利用できるような defclassとdefmethodを定義してみました。

似たようなネタばかりやっている気がします。

(asdf:load-system :alexandria)

;; セッターとゲッター
(defgeneric get-slot (obj slot))
(defgeneric set-slot (obj slot new))
(defmethod get-slot (obj slot)
(slot-value obj slot))
(defmethod set-slot (obj slot new)
(setf (slot-value obj slot) new))

(defmethod (setf get-slot) (new obj slot)
(set-slot obj slot new))

(defgeneric get-parameter (obj param))
(defgeneric set-parameter (obj param new))
(defmethod (setf get-parameter) (new obj param)
(set-parameter obj param new))

(defmethod get-parameter ((this hash-table) param)
(gethash param this))
(defmethod set-parameter ((this hash-table) param new)
(setf (gethash param this) new))

;; ドット区切り、ドル記号区切りのシンボルを変換するマクロ
(defmacro with-accessor-translation (&body body)
(let ((symbol-list
(remove-duplicates (collect-accessor-symbols body))))
`(symbol-macrolet ,(mapcar #'accessor-symbol->symbol-macrolet-form symbol-list)
,@body)))

(defun accessor-symbol? (sym)
(when (symbolp sym)
(let* ((name (symbol-name sym))
(len (length name)))
(when (> len 2)
(let ((subname (subseq name 1 (1- len))))
(or (find #\. subname)
(find #\$ subname)))))))

(defun collect-accessor-symbols (exp)
(let ((symbols (alexandria:flatten exp)))
(remove-if-not #'accessor-symbol? symbols)))

(defun separate-symbol (sym)
(let ((name (symbol-name sym))
(result nil)
(pos 0))
(loop
:with len = (length name)
:for dot-pos = (position #\. name :start pos)
:for dollar-pos =(position #\$ name :start pos)
:while (< pos len)
:do
(cond
((and (null dot-pos) (null dollar-pos))
(push (subseq name pos) result)
(setf pos len))
((or (null dollar-pos)
(and dot-pos (< dot-pos dollar-pos)))
(push (subseq name pos dot-pos) result)
(push :dot result)
(setf pos (1+ dot-pos)))
(t ; (or (null dot-pos) (< dollar-pos dot-pos))
(push (subseq name pos dollar-pos) result)
(push :dollar result)
(setf pos (1+ dollar-pos)))))
(labels ((recur (rest acc)
(if rest
(destructuring-bind (key sel &rest r) rest
(case key
((:dot) (recur r `(get-slot ,acc ',(intern sel))))
((:dollar) (recur r `(get-parameter ,acc ',(intern sel))))
(t (error "invalid keyword"))))
acc)))
(let ((lst (nreverse result)))
(recur (cdr lst) (intern (car lst)))))))

(defun accessor-symbol->symbol-macrolet-form (sym)
`(,sym ,(separate-symbol sym)))

;;; defclass/defmethod マクロの定義
(defmacro defclass* (name (&rest supers) (&rest clauses) &rest options)
`(progn
(defclass ,name ,supers
,(mapcar
#'expand-clause
clauses)
,@options)
,@(mapcar
#'(lambda (clause) (expand-form-in-clause name clause))
clauses)))

(defmacro defmethod* (name (&rest args) &body body)
`(defmethod ,name (,@args)
(with-accessor-translation
,@body)))

(defun couple (lst)
(loop
:for head on lst by #'cddr
:collect (subseq head 0 2)))

(defun expand-clause (clause)
(let ((name (car clause))
(options (couple (cdr clause))))
(let ((result nil))
(dolist (option options)
(case (first option)
((:initform) (push option result))
((:initarg) (push option result))
(T 'nothing-to-do)))
(unless (find :initarg result :key #'first)
(push
`(:initarg ,(alexandria:make-keyword name))
result))
`(,name
,@(apply #'append (nreverse result))))))


(defun expand-form-in-clause (classname clause)
(let ((name (car clause))
(options (couple (cdr clause))))
(let ((result nil))
(dolist (option options)
(cond
((and (eq (first option) :readonly)
(eq (second option) T))
(push `(defmethod set-slot ((this ,classname) (slot (eql ',name)) new)
(error "readonly slot"))
result))
(T 'nothing-to-do)))
`(progn ,@result))))


;; test

(defclass* <person> ()
((id :initform (error "required") :initarg :id :readonly t)
(name)
(age :initarg :age)))

(defmethod* show ((this <person>))
(format t "~A: ~A (~A)~%"
this.id
this.name
this.age))

(defmethod* correct ((this <person>))
(incf this.age))

(defmethod* invalid ((this <person>))
(setf this.id 3))

(defvar p (make-instance '<person> :id 1 :name "kurohuku" :age 23))
(describe p)

(show p)

(correct p)
(describe p)

(invalid p)


2011年6月7日火曜日

Common Lispは自由過ぎてわかりづらいという思い

ブログがソースコード貼り付け場所と化しているので、文章を書く努力。推測とか感覚とかなんとなくがミックスされてるので文章としてどうなんだろう。

本文

Common Lispは数多くあるプログラミング言語の中でも、自由度という点では(大量の括弧を気にしなければ)トップクラスの言語ではないかと思います。

例えば、Common Lispには、プログラマが手を加えることのできる処理のタイミングが3つあります。 1つ目は普通に処理が実行される時、2つ目はコンパイル時(マクロ(※1)/コンパイラマクロ)、3つ目は読み込み時です。多くの言語では、2つ目と3つ目の処理に手を加える機能は存在しないか、限定的なものです。

また、Common Lispでは、プログラマが望むならば、大抵のものは自分で作成できます。組み込みのオブジェクトシステム(CLOS)が気にくわなければ、新しいオブジェクトシステムを作成して組み込めます。あるいは、数値演算を中置記法で書く構文を導入することもできますし、正規表現リテラルの記法を定義することもできます。そうして新しい構文や記法を追加しても、Objective-CLになったり、CL++になったりせずに、依然としてCommon Lispのままです。

自由であることは良いことだと、多くの人が思っているでしょう。けれど、Common Lispがこれだけ自由なのは果たして良いことなのでしょうか。

もちろん、良いことです。

良いことですが、世の中にはリスク無しのリターンも、欠点のない利点もほとんど存在しないだろうと思います。なので、このフリーダムっぷりにも相応の欠点がある、と感じています。

なにが問題かというと、それは「わかりにくい」ということです。少ないと言われるCommon Lispのライブラリですが、それでもLispハッカーな方々がいろいろなライブラリを作成し、公開してくれています。そうしたライブラリを使おうとすると、私のような木っ端Lisp使いは思うわけです。「使い方がわからん」と。

というのも、ライブラリごとにインタフェースがまちまち(※2)で、「他のライブラリではこうだったし、たぶんこうだろうな」という考えが通用しにくいからです。もちろん、ドキュメントや、最悪ソースコードをしっかり読めば理解はできるでしょう。しかし、例えばJavaだったらAPI仕様を斜め読みする程度で使い方はわかります。どの機能(クラス)もだいたい同じような使い方だからです。 .NetやPython、Rubyのライブラリも、Common Lispほどわかりづらいことはないだろうと思います。

JavaもC#もPythonもRubyも、おそらくCommon Lispほど自由ではないです。これらの言語を設計した人たちは、プログラマの自由を制限して、思想や作法を強制しています。けれど、そのおかげでわかりやすいです。

上記の言語には、Common Lispよりもたくさんのユーザーと開発者がいます。制限された自由の中で、同じような作法で作られたライブラリ群を使う方が良い、と考える人が大勢いるということだと思います。ならば、そういった考えを貰ってきても良いはずです。うまくいかなかそうでも、Common Lispが消え去るわけではありませんし。

今後しばらく、短い・わかりやすい・再利用しやすい、けれどそのかわりに思想や作法を強制される、という考えでも良いということを念頭にプログラミングしてみようと思います。

自作マクロの山になって本末転倒な予感がしますが。

※1: 仕様上、マクロがコンパイル時に展開される、という表現は正しくないかと思います。詳しくはHyperSpecあたりを参照してください。 ※2: Common Lispのライブラリのインタフェースがまちまちだ、というのは個人的な印象に過ぎないかもしれません。

2011年6月4日土曜日

リクエストパラメータを引数として扱う

HTTPの説明の際に、URLを関数、リクエストパラメータを引数と例えることがあるようなので、実際に引数=リクエストパラメータとなるようにしてみました。

(asdf:load-system :cl-annot)
(asdf:load-system :alexandria)
(asdf:load-system :clack)

(defpackage annotation
(:nicknames a)
(:export httpfn/clack))

(defpackage http-as-function
(:use :cl)
(:nicknames httpfn)
(:export
:with-http-parameters
:*env*
:*request*))

(in-package :http-as-function)

(defparameter *request* nil)
(defparameter *env* nil)

;; clack.request:body-parameterはパラメータ名の大文字/小文字を区別するため、
;; 区別せずに値を取得できる関数を定義する。
(defun body-parameter-ci (request name)
(let ((plist (slot-value request 'clack.request::body-parameters)))
(loop
:for (k v) on plist by #'cddr
:when (string-equal k name)
:do (return-from body-parameter-ci v))
nil))

(defun query-parameter-ci (request name)
(let ((plist (slot-value request 'clack.request::query-parameters)))
(loop
:for (k v) on plist by #'cddr
:when (string-equal k name)
:do (return-from query-parameter-ci v))
nil))

(defmacro with-http-parameters ((&rest params) request &body body)
(let ((gparamfn (gensym))
(greq (gensym)))
`(let* ((,greq ,request)
(,gparamfn
(case (clack.request:request-method ,greq)
(:post #'body-parameter-ci)
(:get #'query-parameter-ci)
(T #'query-parameter-ci))))
(let
,(mapcar
#'(lambda (param)
(if (listp param)
`(,(car param) (funcall ,gparamfn ,greq ,(cdr param)))
`(,param (funcall ,gparamfn ,greq ,(symbol-name param)))))
params)
,@body))))

(defun defun->httpfn (defun-form)
(destructuring-bind (def name lambda-list &rest body) defun-form
(multiple-value-bind
(forms declarations doc-string)
(alexandria:parse-body body :documentation t)
(multiple-value-bind
(required optional rest key allow-other-keys? aux)
(alexandria:parse-ordinary-lambda-list lambda-list)
(when (or optional rest key allow-other-keys? aux)
(error "lambda-list error"))
`(,def ,name (httpfn:*ENV*)
,doc-string
,@declarations
(let ((httpfn:*REQUEST* (clack.request:make-request httpfn:*ENV*)))
(with-http-parameters ,required httpfn:*REQUEST*
,@forms)))))))

(cl-annot:defannotation a:httpfn/clack (defun-form) (:arity 1)
(defun->httpfn defun-form))


;; test
(cl-annot:enable-annot-syntax)

;; アノテーションを付けた関数
@a:httpfn/clack
(defun show-article (id)
"show the article specified by `id'"
(cond
((string= id "1")
`(200 (:content-type "text/html")
("<html><body>page 1</body></html>")))
(T
`(200 (:content-type "text/html")
("<html><body>unexpected id</body></html>")))))

;; アノテーションを付けない関数
(defun show-article-2 (id)
"show the article specified by `id'"
(cond
((string= id "1")
`(200 (:content-type "text/html")
("<html><body>page 1</body></html>")))
(T
`(200 (:content-type "text/html")
("<html><body>unexpected id</body></html>")))))

;; Clackでは属性リスト形式(key1 value1 key2 valu2 ...)でリクエストの内容が渡される
;;(show-article `(:request-method :get :query-string "id=1"))
;; -> (200 (:CONTENT-TYPE "text/html") ("<html><body>page 1</body></html>"))

;;(show-article-2 "1")
;;-> (200 (:CONTENT-TYPE "text/html") ("<html><body>page 1</body></html>"))

(clack.app.route:defroutes app
(GET "/" #'show-article))

(defparameter *app* (clack:clackup #'app :port 5555))

;;(clack:stop *app*)

Webページとして表示させるために、Clackを利用しました。

Hunchentoot、Clack、cl-annot、Alexandriaをインストール(すべてquicklispでインストール可能)後、上記のコードを実行すると、5555ポートでWebサーバが立ち上がります。

http://localhost:5555/ にアクセスすると、show-articleの呼び出し結果が Webページとして表示されます。クエリストリングを追加して、 http://localhost:5555/?id=1 にアクセスすると、前回とは異なる内容が表示されるかと思います。

show-articleは引数idに応じて戻り値を返す普通の関数ですが、 httpfn/clackをアノテーションとして付加すると、引数名と同名のリクエストパラメータの値を受け取るClack用の関数に変化します。

同等の処理を毎回記述するよりも、アノテーションを付加するだけのほうが楽で良いかなぁ、と思いました。 Webアプリケーション開発の経験値がないので、有用であるかはわかりませんが。