Wordbank

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

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

Android Studio 0.8.6

Android Studio がいつの間にか 0.8.1 から 0.8.6 にアップデートされていた。以前、Android Studio 0.8.1 で作成したプログラムを実機で動かそうとしたときに build.gradle の設定をこのように設定していた。

0.8.6 にアップデートしたついでに設定方法を見直してみた。まずプロジェクトを作成するときに Minimum SDK の項目に API 19: Android 4.4 (KitKat) を指定する (最新は Android L であるがこれを選択するとうまくいかなかった)。API 15 の実機で動作させるため、build.gradle の設定は以下のようにした。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apply plugin: 'com.android.application'
android {
compileSdkVersion 19
buildToolsVersion "20.0.0"
defaultConfig {
applicationId "com.example.tanaka.myapplication"
minSdkVersion 15 // Changed this value from 19 to 15
targetSdkVersion 19
versionCode 1
versionName "1.0"
}
buildTypes {
release {
runProguard false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
}

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