Let Over Lambda

先週から読み始めてようやく読了。先々月、半分くらいまで読んでいたのだが、中断して内容を忘れてしまっていたので最初から一気に読み直した。Let Over Lambdaは一言でいうと、Common Lispのアドバンスドなマクロのテクニックが解説されている非常にためになる本。特にマクロを定義するためのdefmacro自体を拡張するマクロや、aletやalambdaといった代表的なアナフォリックマクロ、パンドリックマクロでクロージャを開くといったLispマクロならではの技術が詰まっている。これらのマクロに共通して見られることだが、本書は一貫して構文を共通化することの重要性を説いていたのが印象的であった。lambdaフォームにはシャープクォート#'をつけないとか、スペシャル変数には耳当てをつけないとか。

筆者の主張としては最初から最後までLispを賛美するのだが、客観性に欠ける部分があったり、効率性に関しては他言語との正当な比較をしていない等、気になるところがないわけではないが、それ以上にマクロの面白さに引き込まれて読んでいて楽しかった。On Lispと並び、個人的にはかなりのヒット本。

行列積

行列積が必要になったので書いてみた。最初は配列に対する操作にしようと思ったけど、手っ取り早く使いたかったのでリストにした。loopマクロを使うとsumしてcollectするだけだから簡単に書けてよい。

1
2
3
4
5
6
7
8
9
(defun matrix-mul (a b)
(let ((n (length (nth 0 a)))
(m (length b)))
(if (= n m)
(loop for i below (length a)
collect (loop for j below (length (nth 0 b))
collect (loop for k below n
sum (* (nth k (nth i a)) (nth j (nth k b))))))
(error "Dimension error"))))

ningler - Blog application sample using ningle

ningle という Common Lisp 用の Web アプリケーションフレームワークを使ってみたところ、非常にシンプルなインタフェースでわかりやすかったので、これを使ったサンプルとして ningler というブログアプリを作ってみました。Python の近いものとして Flask というマイクロフレームを調べていたのですが、そのチュートリアルに Flaskr というサンプルがあったので、ningler はこれを ningle 用に移植したものになります。
テンプレートエンジンには CL-EMB、データベースは SQLite を使いました。データベース用の Common Lisp ライブラリは CL-DBI を使ってます。

こういった Web アプリケーションフレームワークは慣れていないので実装の一つ一つが手探り状態でした。特に ningle を使ってみて困った点は、ある URL から別の URL にリダイレクトするときにデータも一緒に引き継ぐ方法がなかった点と、セッション管理の正当な方法がわからなかった点です。前者は Flask の flash に相当する機能で、リダイレクト後にリプライするデータをバッファに積んでおき、実際にリダイレクト先にリクエストがきたときにバッファから取り出してリプライする、という使い方を想定したもので、Flask ではフレームワークとテンプレートで連携して実現できるようになってます。
一方、ningle にはそのような連携機能はないため、今回作った ningler ではフレームワーク側に文字列をバッファしておき、リダイレクト先にリクエストがきたらバッファから取り出して CL-EMB のテンプレートに env 経由で渡してあげるという方法をとりました。後者に関しては、clack:clackup するときに Clack<clack-middleware-session> を指定した上で、(ningle:context :session) で取得できるデータにログイン状態を記録することで実現しました。これは正しい方法なのかいまいちわかってません。

また、CL-EMB が他のテンプレートを埋め込む機能がなかった点に関しては、埋め込む部分を <% @var body %> としておき、他のテンプレートを execute-emb した結果を env から body に渡して埋め込みました。

こんな感じで色々ケアする部分はあるようですが、複雑なフレームワークを覚えるよりは ningle のようなシンプルなフレームワーク上に色々組み合わせて構築していく方が小回りがきくため、プロトタイピングに向いていると感じました。それにしても nitro_idiot さんはこのような便利な Common Lisp ライブラリをいくつも精力的に開発されていてすごいっす。

参考

Common Lisp の効率的なファイル読み込み

Common Lisp でファイルを読み込む方法は Slurping a file in Common Lisp が参考になる。指定したファイルを読み込み、バッファ (配列) を返す関数はこんな感じ。実測はしていないが Perl スクリプト以上の速度が出るらしい。

1
2
3
4
5
(defun read-file (path)
(with-open-file (in path)
(let ((seq (make-array (file-length in) :element-type 'character :fill-pointer t)))
(setf (fill-pointer seq) (read-sequence seq in))
seq)))

Wordbank

Wordbank に検索回数順に表示するコマンド :t を追加した。何回も検索している単語はおそらく覚えにくい単語だと思うので、これで自分の苦手単語をチェックしてみよう。

DB から検索回数の多い順に N 個の要素を取り出す処理 (nthmost) は、DB のすべての要素をソートした上で上位 N 個を取り出すという非常にナイーブな実装にしている。これは On Lisp にあるように、N が DB の要素数によらず小さい定数である場合は、上位 N 個の要素を記憶しながら DB のすべての要素を調べあげ、記憶していた上位 N 個をソートして返す、というアルゴリズムの方が効率がよい。DB のサイズが大きくなってパフォーマンスが悪くなってきたら対応してみるかな。

Wordbank

Wordbank の検索機能を強化してみた。デ辞蔵の API では検索ワードから始まる単語が複数返されるので、検索ワードが検索結果の第一候補と一致した場合、もしくは検索結果が 1 件のみの場合はそのまま意味を表示し、その他の場合は複数の検索結果からユーザが選択するようにした。また、検索ワードの語尾に * を指定することで、第一候補と一致した場合でも複数の検索結果を表示するようなインタフェースにした。

さらに、一度調べた検索結果はローカルの DB にキャッシュするようにして、次回以降は高速に検索できるようにした。無駄に検索クエリを投げないので Web サービスにも優しい。

コマンドは vi ライクにしている。:q! とか :wq とかも欲しいけど現状は未対応。

  • :q プログラムを終了
  • :w ローカル DB を保存

あとは単語ごとに検索回数を記録してソートして表示できるようにすれば、とりあえず欲しかった機能は満たされるかな。

ちょっと困っているのがコマンド入力インタフェースの実装。検索ワードを入力する状態と、検索結果から選択する状態があるため、状態を持ったクロージャがあれば何とかなるかと思っていたが、第一候補に一致した場合の検索結果の一発表示やコマンドの追加が重なって次第にカオスな感じになってきており、なんか違うなーという感じ。あるべき姿はこうではなく、インタフェースの改良やコマンドの追加に対して柔軟に対応できる拡張性のある何か。

Wordbank - 英和辞書用コマンドラインインタフェース

Common Lisp で何かプログラムを作ろうと思い立ち、Wordbank というものを作りました。これはコマンドプロンプトに入力した英単語の意味を表示するだけのプログラムですが、辞書で引いた単語の履歴を覚えておいて、後で復習できるような目的で作成しています(履歴機能は未実装)。こだわったところは、SLIME 上でコンパイルしたり Quicklisp でロードできるようにしつつ、出来上がった Lisp のソースコードをそのままシェルからスクリプト実行できるようにした点です。複数ファイルから構成される場合にスクリプト実行する方法はもう少し工夫する必要がありそうです。

問題の辞書ですが、デ辞蔵 REST版API という Web サービス があったので利用させて頂きました。現状、API の動作確認用プロトタイプという段階であり、クエリを投げて結果を表示することしかできませんが、引き続き履歴機能の実装と検索機能の拡充を進めていく予定です。

ちなみに HTTP リクエストには Drakma、検索結果からの情報抽出には CL-PPCRE という正規表現ライブラリを利用しました。Common Lisp では package.lisp にパッケージ情報やら asdf ファイルに依存関係やらを書くことが多いようですが、CL-Project というスケルトン生成ツールはユニットテストの設定もしてくれるので便利でした。同様のツールで Quickproject がありますが、こちらはユニットテストの設定はしないようです。

get-output-stream-string

get-output-stream-string は指定したストリームに出力された文字を出力順のまま文字列として返す関数である。関数が実行される際に、ストリーム中のすべての文字はクリアされる。Hyperspec の get-output-stream-string によると、 make-string-output-stream 以外で生成したストリームに関しての動作は未定義とされている。Practical Common Lisp のサンプルプログラムに with-output-to-string で生成したストリームから get-output-stream-string で文字列として取り出しているコードがあるが、言語仕様上はよろしくないようだ。

Common Lisp (SBCL) でスクリプトを書く方法

Common Lisp (SBCL) でスクリプトを書くいい方法が見つからなかったので調べてみた。重要なのは次の三点。

  • Quicklisp をロードしたイメージファイルの生成
  • シェルスクリプト shebang からの exec sbcl ハック
  • scripted main はシェルスクリプトに与えた $0 から判定する

順を追って説明します。

Quicklisp をロードしたイメージファイルの生成

スクリプト実行するたびに Quicklisp を実行しているとロードに時間がかかるので、あらかじめ Quicklisp をロードした状態のイメージファイル (以下の例では sbcl-base.core という名前のイメージファイル) を生成し、スクリプト実行時に指定する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ sbcl [~/dev/tmp]
This is SBCL 1.2.1, an implementation of ANSI Common Lisp.
More information about SBCL is available at <http://www.sbcl.org/>.
SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses. See the CREDITS and COPYING files in the
distribution for more information.
* (sb-ext:save-lisp-and-die "sbcl-base.core" :executable t)
[undoing binding stack and other enclosing state... done]
[saving current Lisp image into sbcl-base.core:
writing 5744 bytes from the read-only space at 0x0x20000000
writing 3120 bytes from the static space at 0x0x20100000
writing 53903360 bytes from the dynamic space at 0x0x1000000000
done]

イメージファイルを指定して sbcl を起動するには —core オプションに指定すればよい。

シェルスクリプト shebang からの exec sbcl ハック

sbcl を直接 shebang で実行するのではなく /bin/sh 経由で sbcl に exec する。--core オプションは先ほど生成した Quicklisp をロード済みのイメージファイルで起動するために指定している。

1
2
3
4
5
#!/bin/sh
#|
exec sbcl --core sbcl-base.core --script $0 $0 "$@"
|#
;;; code comes here

このとき、最初の $0 はこのファイル自身をスクリプト実行するために与えている。2 番目の $0 は後述する scripted main であることを判定するために使う。最後の "$@" は残りの全引数を sbcl に与えている。

2 行目の #|# で始まるためシェルスクリプトからはコメント行に解釈される。一方、Common Lisp からは #| から |# まではコメントであると見なされるためスクリプト実行時は無視される。ただし、最初の shebang の行のみは Common Lisp の文法では解釈できないため、.sbclrc に読み飛ばす設定をする。

.sbclrc
1
2
3
4
5
(set-dispatch-macro-character #\# #\!
(lambda (stream character n)
(declare (ignore character n))
(read-line stream nil nil t)
nil))

scripted main はシェルスクリプトに与えた $0 から判定する

scripted main とはスクリプトとして実行された際のエントリポイントを表す造語。例えば、Python ならば

1
2
3
def main():
# ...
main() # call main function directly

のように実際の処理をべた書きしてしまうと、このスクリプトをモジュールとして外部に公開するときに、import するたびに main() が実行されてしまい困ることになる。そこで、

1
2
if __name__ == '__main__':
main() # scripted main

のように __name__ 変数でスクリプト実行かどうかを条件判定すれば、このモジュールをスタンドアロンなスクリプトとして実行するときは main() が実行され、外部から import される場合には main() は実行されずに必要な機能のみ読み込ませることが可能になる。

これと同じことを SBCL で実現するにはコマンド実行時のみコマンドライン引数がセットされる sb-ext:*posix-argv* を使い、(pathname-name *load-truename*) で与えられる文字列が見つかればスクリプト実行なので scripted main を実行、見つからなければ REPL や外部ファイルから load されたものとして scripted main は実行しない、というように見分ける作戦にする。

1
2
3
4
5
;; scripted main
(when (member (pathname-name *load-truename*)
sb-ext:*posix-argv*
:test #'(lambda (x y) (search x y :test #'equalp)))
(main))

しかし、普通に sbcl --script file <filename> のようにファイルを指定して実行しただけでは <filename> という文字列は sb-ext:*posix-argv* に含まれないという問題がある。そこで、sbcl を exec するときに --script $0 の後に重ねて $0 を指定することで、shebang から起動された場合は sb-ext:*posix-argv* の第二要素に必ずスクリプトのファイル名を入れることにする。(第一要素は sbcl になる)

以上をまとめるとスクリプト全体は次のようになる。

myscript.lisp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/sh
#|
exec sbcl --core sbcl-base.core --script $0 $0 "$@"
|#
(let* ((*standard-output* (make-broadcast-stream))
(*error-output* *standard-output*))
(ql:quickload "split-sequence"))
(in-package :cl-user)
(defpackage :myscript
(:use #:cl #:split-sequence))
(in-package :myscript)
;;; code comes here
(defun main (argv)
(format t "~a~%" (first argv))
(format t "~s~%" (split-sequence #\, (first argv))))
;; scripted main
(when (member (pathname-name *load-truename*)
sb-ext:*posix-argv*
:test #'(lambda (x y) (search x y :test #'equalp)))
(main (cddr sb-ext:*posix-argv*)))

コマンドライン引数で渡した文字列を split-sequence を使って , でリストに分割して表示するだけのスクリプト。split-sequence は Quicklisp からロードしているが、ロード中のメッセージを抑制するために、*standard-output**error-output* の束縛を一時的に変更して端末に出力されないようにしている。

1
2
3
$ ./myscript.lisp "hello, world"
hello, world
("hello" " world")

参考

Writing Scripts with Common Lisp

Lisp の map 関数

Lisp にはリスト操作の map* 関数が 6 種類あるが違いが微妙なのでまとめてみた。まずは基本形の mapcar/maplist を押さえておけばいい感じ。

mapcar/maplist

mapcar は引数にとったリストを構成する各コンスセルの car 部に対して関数を適用し、その結果をリストとして返す。

1
2
3
4
5
(mapcar #'(lambda (x) (* x 2)) '(1 2 3))
;; => (2 4 6)
(mapcar #'+ '(1 2 3) '(4 5 6))
;; => (5 7 9)

maplist は引数にとったリストを構成する各コンスセルの cdr 部に対して関数を適用し、その結果をリストとして返す。

1
2
3
4
5
(maplist #'list '(1 2 3))
;; => (((1 2 3)) ((2 3)) ((3)))
(maplist #'list '(1 2 3) '(4 5 6))
;; => (((1 2 3) (4 5 6)) ((2 3) (5 6)) ((3) (6)))

mapcan/mapcon

mapcan は mapcar 同様に、引数にとったリストを構成する各コンスセルの car 部に対して関数を適用するが、その結果がリストであることを期待して nconc したものを返す。

1
2
3
4
5
6
7
(mapcan #'list '(1 2 3))
;; => (nconc '(1) '(2) '(3))
;; => (1 2 3)
(mapcan #'list '(1 2 3) '(4 5 6))
;; => (nconc '(1 4) '(2 5) '(3 6))
;; => (1 4 2 5 3 6)

mapcon は maplist 同様に、引数にとったリストを構成する各コンスセルの cdr 部に対して関数を適用するが、その結果がリストであることを期待して nconc したものを返す。

1
2
3
4
5
6
7
(mapcon #'list '(1 2 3))
;; => (nconc '(1 2 3) '(2 3) '(3))
;; => ((1 2 3) (2 3) (3))
(mapcon #'list '(1 2 3) '(4 5 6))
;; => (nconc '((1 2 3) (4 5 6)) '((2 3) (5 6)) '((3) (6)))
;; => ((1 2 3) (4 5 6) (2 3) (5 6) (3) (6))

mapc/mapl

mapc/mapl のリスト処理は mapcar/maplist と同様であるが、戻り値は一番目に与えたリストを返すという点で異なる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(mapc #'(lambda (x) (format t "This is ~a~%" x)) '(1 2 3))
;; This is 1
;; This is 2
;; This is 3
;; => (1 2 3)
(mapc #'(lambda (x y) (format t "~a + ~a = ~a ~%" x y (+ x y))) '(1 2 3) '(4 5 6))
;; 1 + 4 = 5
;; 2 + 5 = 7
;; 3 + 6 = 9
;; => (1 2 3)
(mapl #'(lambda (x) (format t "This is ~a~%" x)) '(1 2 3))
;; This is (1 2 3)
;; This is (2 3)
;; This is (3)
;; => (1 2 3)
(mapl #'(lambda (x y) (format t "append ~a and ~a => ~a~%" x y (append x y))) '(1 2 3) '(4 5 6))
;; append (1 2 3) and (4 5 6) => (1 2 3 4 5 6)
;; append (2 3) and (5 6) => (2 3 5 6)
;; append (3) and (6) => (3 6)
;; => (1 2 3)

map/mapinto (おまけ)

map は一般的なシーケンス型に適用できる mapcar の汎用版。

1
2
3
4
(map 'vector #'+ #(1 2 3) #(4 5 6))
;; => #(5 7 9)
(map 'list #'+ '(1 2 3) '(4 5 6))
;; => (5 7 9)

mapinto は mapcar の結果を第一引数に指定した変数に割り当てる。

1
2
3
4
(map-into a #'+ '(1 2 3) '(4 5 6))
;; => (5 7 9)
a
;; => (5 7 9)