ブログトップ 記事一覧 ログイン 無料ブログ開設

翡翠はコンピュータに卵を生むか

2016-12-02

NumPy vs Common Lisp 実行時間のみの比較

NumPyとCommon Lispの速度比較記事。処理系の起動時間も測ってしまっているので処理自体にかかっている時間を測って比較してみる。

環境は

  • Core i5 4670 3.40 GHz
  • Ubuntu 14.04 LTS

Python 3.4.3 + Numpy

import numpy as np
import time

N = 100000

# Python版
def sumup(n):
    return sum(range(1, n + 1))

# NumPy版
def sumup(n):
    return np.arange(1, n + 1).sum()

def main():
    print("python with numpy start.")
    result = {}
    for count in range(1, N + 1):
        result[count - 1] = sumup(count)
    print("python with numpy end.")

start = time.time()
main()
elapsed_time = time.time() - start
print("elapsed_time:{0}".format(elapsed_time))

SBCL 1.3.11

(defparameter *n* 100000)

;; 1. 引数のみ型宣言
(defun sumup1 (n)
  (declare (type fixnum n))
  (let ((sum 0))
    (loop for i from 1 to n
          do (incf sum i))
    sum))

;; 2. 引数と局所変数で型宣言
(defun sumup2 (n)
  (declare (type fixnum n))
  (let ((sum 0))
    (declare (type fixnum sum))
    (loop for i from 1 to n
          do (incf sum i))
    sum))

;; 3. 引数と局所変数で型宣言 + 最適化宣言、実行時型チェック無効
(defun sumup3 (n)
  (declare (type fixnum n)
           (optimize (speed 3) (safety 0)))
  (let ((sum 0))
    (declare (type fixnum sum))
    (loop for i from 1 to n
          do (incf sum i))
    sum))

;; 4. 引数と局所変数で型宣言 + 最適化宣言、実行時型チェック無効、loop内で使う変数iも型宣言
(defun sumup4 (n)
  (declare (type fixnum n)
           (optimize (speed 3) (safety 0)))
  (let ((sum 0))
    (declare (type fixnum sum))
    (loop for i fixnum from 1 to n
          do (incf sum i))
    sum))

;; 5. 引数と局所変数で型宣言 + 最適化宣言、実行時型チェック無効、loop内で使う変数iも型宣言、引数と返値の型を宣言する
(declaim (ftype (function (fixnum) fixnum) sumup5))
(defun sumup5 (n)
  (declare (type fixnum n)
           (optimize (speed 3) (safety 0)))
  (let ((sum 0))
    (declare (type fixnum sum))
    (loop for i fixnum from 1 to n
          do (incf sum i))
    sum))

(defun main ()
  (print "common lisp start.")
  (loop for count from 1 to *n*
        collect (sumup5 count))
  (print "common lisp end."))

(time (main))

結果

結果はこのようになる。

ftypeの宣言があまり効いていないが、なんにせよCommon Lispに適切なチューニングを施すことによってNumPyを使ったときよりも3倍以上も速くなっていることが分かる。

2016-12-01

インストール不要でブラウザ上からSchemeを試せるサイトscheme.contellas.comを作った

Common LispでWebアプリケーションを作る練習として、簡単なSchemeの開発環境的なものを作った。

  • Javascript製のSchemeインタプリタBiwaschemeにエディタ(CodeMirror)をつけたもの
  • HTML5の機能を使っているので対応したブラウザが必要 (Chrome、Firefox、IE11で確認済)
    • 書いたコードをブラウザのローカル領域に保存できる(Local Storage)
    • Schemeインタプリタは別スレッド(Web Worker)で動いており、無限ループに入ってしまったときなどにSchemeインタプリタだけ再起動できる(Killボタン)
  • 評価
    • 何も選択せずにEvalボタンを押すとファイル全体が評価される
    • 領域を選択してEvalボタンを押すとその領域が評価される
    • 括弧にカーソルを合わせてハイライト表示させた上で Ctrl+J でその式だけを評価できる(式単位の評価)
  • 自動インデント
    • Returnで改行するとその時点で適切な位置に移動する
    • Tabを押すとその行がインデントされる
    • 領域を選択してTabを押すとその領域がインデントされる
  • ファイル操作
    • Newボタンでファイルの新規作成
    • ファイルを選択した状態でRenameで名称変更、Deleteで削除ができる
    • 何かコードを評価したときや、ファイル操作をしたタイミングで状態が自動保存される
    • 右上の設定ボタンからClear localStorageを選ぶとブラウザに保存されていた情報が全て削除される

Schemeインタプリタの仕様はBiwaschemeのサイトを参照。R7RS対応とされているがマクロ定義は伝統的マクロだったりと少し異なる部分もある。

BiwaschemeではSchemeインタプリタからJavascriptのコードも呼べるが、Web Worker上で動いているのでSchemeインタプリタからJavascriptを呼んでもDocumentやWindowはいじれない。

TODO

  • (load "file-name") で localStorageからファイルを読み込めるようにする
  • ファイルをzipでまとめてダウンロードできるようにする
  • カラーテーマ、キーバインドの設定などの個人設定
  • 領域でコメント、アンコメント

2016-11-27

Emacs+SLIMEでCommon Lispの開発をリモートでするまとめ

ディープラーニングなどの重い処理を含むプログラムを出先から開発したいというときがよくある。

そういうときまずやるのが、SSHでGPUを積んだ開発マシンにリモートログインしてターミナル上でEmacs/Vimで開発するというやり方だが、LTEで速くなったとはいえ入力の反映がもたつくことがよくあり、非常にストレスが溜まる。moshも使ってみたが大して差は感じられなかった。その他にもCtrlとMeta以外のモディファイアキーを使いたいなど、やっぱりGUI版のEmacsで開発したいと思うことがよくある。

SLIMEはLisp処理系側で動くSwankと呼ばれるサーバと、それに接続するEmacs側フロントエンドの機能で構成されている。このサーバとクライアントの通信はローカルネットワークに限らないので、リモートからLisp処理系に接続することもできるのだ。知らないけど同じSwankクライアントであるslimv.vimやatom-slimeでも原理的にはできるはずである。

1. SSHでSwankのポートをポートフォワーディングして開発マシンに接続する

ホストの設定は~/.ssh/configでしておく。

Host myhost
    HostName xxx.yyy.com
    Port 22
    User hoge
    identityfile ~/.ssh/myhost_rsa

開発マシンの4005番をローカル(出先マシン)の4005番に繋げるには、

hoge@local$ ssh -L 4005:localhost:4005 myhost

2. 開発マシンでSwankサーバを立ち上げる

接続が切れても再開できるようにscreen上でLisp処理系を立ち上げる。

hoge@remote$ screen -S swank ros -Q -L sbcl-bin dynamic-space-size=4096

3. Swankサーバを起動する

(require :swank)
(swank:create-server :port 4005)

これでサーバの4005番をSSH経由でローカルの4005番に繋げられた。もちろんポート番号は自由に設定できる。

4. ポートフォワーディングしたローカルのポートに対してslime-connectする。

SLIMEをインストール済のEmacsを起動して、 M-x slime-connect RET RET RET でlocalhost:4005に接続して開発マシンのREPLがローカルのEmacs上に現われる。この時、「ローカルのSLIMEとリモートのSwankサーバのバージョンが違うけど大丈夫?」というプロンプトが出ることがある。これが邪魔なときは.emacsに以下のように書いておく。

(setq slime-protocol-version 'ignore)

一連のコマンドをまとめる

ややこしいので2、3のコマンドで済むようにしておく。まずローカルの~/.bashrcなどにエイリアスを書いておく。

alias ssh-swank='ssh -L 4005:localhost:4005 myhost'

次に3.のSwankサーバを起動するLispコードをファイルに保存して開発マシンの~/swank-start.lispに置いておく。それから開発マシンの~/.bashrcなどに処理系を起動してSwank起動コードを読み込むためのエイリアスを書く。

alias swank-start="screen -S swank ros -Q -L sbcl-bin dynamic-space-size=4096 run -l $HOME/swank-start.lisp"

なお、sshで接続すると~/.bashrcを読まないらしいので、~/.bash_profile に~/.bashrcを読むように書いておく。

if [ -f ~/.bashrc ]; then
    . ~/.bashrc
fi

これでターミナルからssh-swank、パスフレーズ入力、swank-start、Emacs起動、M-x slime-connect RET RET RETでREPLが立ち上がる。

TRAMPでリモートのファイルを編集する

リモートのファイルを編集するにはEmacsに標準搭載されているTRAMPが使える。 ~/.emacs にこう書いておく。

(require 'tramp)
(setq tramp-default-method "scp")
(setq tramp-auto-save-directory "/tmp")

オートセーブの度にリモートと通信しようとしてEmacsが止まるので、オートセーブファイルはローカルの/tmpに保存するようにしておく。これで単にC-x C-f /ssh:myhost:~/file.lisp RET などとすることによりリモートのファイルを開ける。

magitも動く!

驚くべきことにtrampでリモートのファイルを開いた状態でM-x magit-statusすると普通にmagitが使えるし、コミットもプッシュもできることに気付く。ただし通信が発生するので多少待たされはする。

まとめ

  • サーバ側でSwankサーバを立てておくことにより出先からEmacs+SLIMEでリモート開発できる
    • 出先のノートPCで重い処理を回してもCPU使用率が最低レベルなのでバッテリーも長持ち
    • ローカル/リモートに関わらずLisp式単位で開発マシンのREPLで評価できる
  • リモートのファイルの編集はEmacsの機能TRAMPによりできる
    • magitもリモートでできる
    • ローカルのEmacsの編集機能がそのまま使えるためにキビキビ動く。低速回線でもストレスがない。

2016-11-22

Common Lispによる線形分類器ライブラリcl-online-learningを書いた

去年、オンライン機械学習本(クマ本)を読んで線形分類器を実装する記事を書いたり、それらのアルゴリズムをまとめてcl-online-learningというライブラリを作ってLispmeetupで紹介したりした。

その後放置していたのだが、最近になってもはや使わないようなアルゴリズムは削除したり、疎ベクトルへの対応や、学習器のCLOSオブジェクトを単なる構造体にするなどの大きな変更をした。このあたりで一度ちゃんと紹介記事を書いておこうかと思う。

cl-online-learningの特徴は、

  • アルゴリズム: パーセプトロン、AROW、SCW-I (おすすめはAROW)
  • 二値分類、多値分類に対応 (one-vs-one、one-vs-rest)
  • データが密ベクトル、疎ベクトルのどちらの場合にも対応
  • 純Common LispでC/C++のライブラリ(AROW++)を上回る速度

インストール

local-projectsディレクトリにソースを展開する。

cd ~/quicklisp/local-projects/
git clone https://github.com/masatoi/cl-online-learning.git

あるいは、Roswellがインストールされているなら単に

ros install masatoi/cl-online-learning

データの読み込み

1つのデータはラベル(+1/-1)と入力ベクトルのペア(cons)で、データのシーケンスがデータセットとなる。 libsvm datasetsの形式のファイルからデータセットを作るには、read-data関数が使える。とりあえずデータはlibsvm datasetsの二値分類データからa1aを使うことにする。

(defpackage :clol-user
  (:use :cl :cl-online-learning :cl-online-learning.utils :cl-online-learning.vector))

(in-package :clol-user)

(defparameter a1a-dim 123)
(defparameter a1a-train (read-data "/path/to/a1a"   a1a-dim))
(defparameter a1a-test  (read-data "/path/to/a1a.t" a1a-dim))

(car a1a-train)
;; (-1.0d0
;;  . #(0.0d0 0.0d0 1.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 1.0d0 0.0d0
;;      0.0d0 1.0d0 0.0d0 0.0d0 0.0d0 0.0d0 1.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0
;;      0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0
;;      0.0d0 0.0d0 1.0d0 0.0d0 0.0d0 1.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0
;;      0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 1.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0
;;      0.0d0 0.0d0 0.0d0 1.0d0 0.0d0 0.0d0 1.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0
;;      1.0d0 0.0d0 1.0d0 1.0d0 0.0d0 0.0d0 0.0d0 1.0d0 0.0d0 0.0d0 1.0d0 0.0d0
;;      0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0
;;      0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0
;;      0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0
;;      0.0d0 0.0d0 0.0d0))

モデル定義

学習器のモデルは単なる構造体で、make-系関数で生成できる。その際いずれもデータの次元数を必要とする。その他にAROWは1個、SCWは2個のメタパラメータを指定する必要がある。パーセプトロン、AROW、SCWのモデルをまとめて定義すると、

(defparameter perceptron-learner (make-perceptron a1a-dim))
(defparameter arow-learner (make-arow a1a-dim 10d0))        ; gamma > 0
(defparameter scw-learner  (make-scw  a1a-dim 0.9d0 0.1d0)) ; 0 < eta < 1 , C > 0

訓練

データ1個を学習するには各学習器のupdate関数を使う。AROWならarow-update関数。これにデータの入力ベクトルとラベルを与えることで、arow-learnerが破壊的に更新される。

(arow-update arow-learner (cdar a1a-train) (caar a1a-train))
;; #S(AROW
;;  :INPUT-DIMENSION 123
;;  :WEIGHT #(0.0d0 0.0d0 -0.04d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0) ...
;;  :BIAS -0.04d0
;;  :GAMMA 10.0d0
;;  :SIGMA #(1.0d0 1.0d0 0.96d0 1.0d0 1.0d0 1.0d0 1.0d0 1.0d0 1.0d0 1.0d0) ...
;;  :SIGMA0 0.96d0)

これをデータセット全体に対して行うのがtrain関数である。

(train arow-learner a1a-train)

予測

こうして学習したモデルを使って、ある入力ベクトルに対して予測を立てるには各学習器のpredict関数を使う。AROWならarow-predict関数。

(arow-predict arow-learner (cdar a1a-test))
;; 1.0d0

正解の値(caar a1a-test)が-1.0d0なのでここは外してしまっている。

これをテストデータ全体に対して行ない、正答率を返すのがtest関数である。

(test arow-learner a1a-test)
;; Accuracy: 84.44244%, Correct: 26140, Total: 30956

となって84%弱の精度が出ていることが分かる。

マルチクラス分類

データの読み込み (MNIST)

マルチクラス分類ではデータのラベルが+1/-1ではなく、0以上の整数になる。例えばlibsvm datasetsからMNISTのデータを落としてきて読み込んでみる。読み込みはread-data関数にmulticlass-pキーワードオプションをつけて呼び出す。

(defparameter mnist-dim 780)
(defparameter mnist-train (read-data "/home/wiz/tmp/mnist.scale" mnist-dim :multiclass-p t))
(defparameter mnist-test  (read-data "/home/wiz/tmp/mnist.scale.t" mnist-dim :multiclass-p t))
;; このデータセットはラベルが1からではなく0から始まるので1足しておく
(dolist (datum mnist-train) (incf (car datum)))
(dolist (datum mnist-test)  (incf (car datum)))

(car mnist-train)
;; (5 . #(0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 ...))
モデル定義

マルチクラス分類は二値分類器の組み合わせで実現する。組み合せ方には色々あるが、cl-online-learningではone-vs-oneとone-vs-restを用意している。一般にone-vs-oneの方が精度が高いが、クラス数の二乗に比例する二値分類器が必要になる。一方のone-vs-restはクラス数に比例する。

例えばone-vs-oneで、二値分類器としてAROWを用いる場合の定義はこうなる。

(defparameter mnist-arow
  (make-one-vs-one mnist-dim    ; データの次元
                   10           ; クラス数
                   'arow 10d0)) ; 二値分類器の型とそのパラメータ

この構造体に対しても二値分類のときと同じくone-vs-one-update、one-vs-one-predict関数でデータを一つずつ処理できるし、train、test関数でデータセットをまとめて処理できる。

訓練、予測

データセットを8周訓練する時間を計測し、テストを行うコードは以下のようになる。

(time (loop repeat 8 do (train mnist-arow mnist-train)))
(test mnist-arow mnist-test)
;; Evaluation took:
;;   3.946 seconds of real time
;;   3.956962 seconds of total run time (3.956962 user, 0.000000 system)
;;   100.28% CPU
;;   13,384,797,419 processor cycles
;;   337,643,712 bytes consed

;; Accuracy: 94.6%, Correct: 9460, Total: 10000
liblinearの場合

高速な線形分類器とされるliblinearで同じデータを学習してみる。

wiz@prime:~/tmp$ time liblinear-train -q mnist.scale mnist.model
real    2m26.804s
user    2m26.668s
sys     0m0.312s

wiz@prime:~/tmp$ liblinear-predict mnist.scale.t mnist.model mnist.out
Accuracy = 91.69% (9169/10000)

こちらはデータの読み込みなども含めた時間なのでフェアな比較ではないが、大まかにいってcl-online-learningの方が大幅に速いといえる。また精度もcl-online-learning(AROW + one-vs-one)の方が良い。ちなみにliblinearのマルチクラス分類はone-vs-restを使っているらしい。

疎なデータの分類

a1aのデータを見ると気付くのは、ほとんどの要素が0の疎(スパース)なデータであるということだ。例えば「単語が文書に出現する回数」のような特徴量は高次元かつスパースになる。これをそのまま扱うと空間計算量も時間計算量も膨れ上がってしまうので、このようなデータではデータの次元数の長さのベクタを用意するのではなく、非零値のインデックスと値のペアだけを保持しておけばいい。 cl-online-learning.vectorパッケージに定義されているsparse-vector構造体がそれで、インデックスのベクタと値のベクタをスロットに持つ。

(make-sparse-vector
 (make-array 3 :element-type 'fixnum :initial-contents '(3 5 10))
 (make-array 3 :element-type 'double-float :initial-contents '(10d0 20d0 30d0)))

;; #S(CL-ONLINE-LEARNING.VECTOR::SPARSE-VECTOR
;;    :LENGTH 3
;;    :INDEX-VECTOR #(3 5 10)
;;    :VALUE-VECTOR #(10.0d0 20.0d0 30.0d0))

疎ベクトルの形でデータセットを読み込むにはread-data関数にsparse-pキーワードオプションをつけて呼び出す。試しに、1355191次元という超高次元のデータセットnews20.binaryを読み込んでみる。1つのデータはラベルとsparse-vector構造体のペアになっていることが分かる。

(defparameter news20.binary-dim 1355191)
(defparameter news20.binary (read-data "/home/wiz/datasets/news20.binary" news20.binary-dim :sparse-p t))

(car news20.binary)
;; (-1.0d0
;;  . #S(CL-ONLINE-LEARNING.VECTOR::SPARSE-VECTOR
;;       :LENGTH 3645
;;       :INDEX-VECTOR #(0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
;;                       ...
;;                       3636 3637 3638 3639 3640 3641 3642 3643 3644)
;;       :VALUE-VECTOR #(0.01656300015747547d0 0.01656300015747547d0
;;                       ...
;;                       0.01656300015747547d0)))

データ中の非零値の数をヒストグラムにしてみるとこうなる。

(ql:quickload :clgplot)
(clgp:plot-histogram (mapcar (lambda (d) (clol.vector::sparse-vector-length (cdr d)))
                             news20.binary) 200 :x-range '(0 3000))

f:id:masatoi:20161122213909p:image

1355191次元といってもほとんどのデータが2000次元以下なので疎なデータであることが分かる。

これを学習するためには、二値分類器としてarowの代わりにsparse-arowを使う。同様にパーセプトロンやSCWにもスパース版がある。

(defparameter news20.binary.arow (make-sparse-arow news20.binary-dim 10d0))
(time (loop repeat 20 do (train news20.binary.arow news20.binary)))
(test news20.binary.arow news20.binary)
;; Evaluation took:
;;   1.588 seconds of real time
;;   1.588995 seconds of total run time (1.582495 user, 0.006500 system)
;;   [ Run times consist of 0.006 seconds GC time, and 1.583 seconds non-GC time. ]
;;   100.06% CPU
;;   5,386,830,659 processor cycles
;;   59,931,648 bytes consed

;; Accuracy: 99.74495%, Correct: 19945, Total: 19996
AROW++の場合

同じことをC++によるAROW実装のAROW++を使ってやってみる。

wiz@prime:~/datasets$ arow_learn -i 20 news20.binary news20.binary.model.arow 
Number of features: 1355191
Number of examples: 19996
Number of updates:  37643
Done!
Time: 9.0135 sec.

wiz@prime:~/datasets$ arow_test news20.binary news20.binary.model.arow 
Accuracy 99.915% (19979/19996)
(Answer, Predict): (t,p):9986 (t,n):9993 (f,p):4 (f,n):13
Done!
Time: 2.2762 sec.
liblinearの場合
wiz@prime:~/datasets$ time liblinear-train -q news20.binary news20.binary.model

real    0m2.800s
user    0m2.772s
sys     0m0.265s
wiz@prime:~/datasets$ liblinear-predict news20.binary news20.binary.model news20.binary.out
Accuracy = 99.875% (19971/19996)

なおAROW++もliblinearもベクトルの内部表現は疎ベクトルでやっている模様。

疎なデータの分類(マルチクラス)

マルチクラス分類の場合でも同じことができるので、MNISTでやってみる。MNISTも画像データではあるが、六割程度は0なので疎なデータといえる。

この場合は疎ベクトルかつマルチクラス分類なので、read-data関数にsparse-pとmulticlass-pの両方のオプションをつけてデータを読み込む。密なデータの時と同様にmake-one-vs-oneを使うが、その引数にsparse-arowを指定するところが異なる。あとは大体一緒。

(defparameter mnist-train.sp (read-data "/home/wiz/tmp/mnist.scale" mnist-dim :sparse-p t :multiclass-p t))
(defparameter mnist-test.sp  (read-data "/home/wiz/tmp/mnist.scale.t" mnist-dim :sparse-p t :multiclass-p t))
;; このデータセットはラベルが1からではなく0から始まるので1足しておく
(dolist (datum mnist-train.sp) (incf (car datum)))
(dolist (datum mnist-test.sp)  (incf (car datum)))

(defparameter mnist-arow.sp (make-one-vs-one mnist-dim 10 'sparse-arow 10d0))
(time (loop repeat 8 do (train mnist-arow.sp mnist-train.sp)))

;; Evaluation took:
;;   1.347 seconds of real time
;;   1.348425 seconds of total run time (1.325365 user, 0.023060 system)
;;   [ Run times consist of 0.012 seconds GC time, and 1.337 seconds non-GC time. ]
;;   100.07% CPU
;;   4,570,387,768 processor cycles
;;   337,618,400 bytes consed

となって、約3倍の高速化となっている。

まとめ

  • 純Common Lispで線形分類器を書いた
    • 学習器をCLOSオブジェクトではなく単なる構造体にしたり、ベクトル演算の実行時の型チェックを外す、学習器の構造体を破壊的に更新して一時的なデータ構造を作らないなどのチューニングにより訓練部分はかなり速い。
    • AROWとSCW-Iは共分散行列の対角成分だけを使う近似をしている
  • コマンドラインベースのliblinearなどとは立ち位置が違うかも
  • Common Lispにはmecab互換の形態素解析エンジンcl-igoもあるので、文書分類などに応用できるかも

2016-11-07

lem: Common Lispで書かれたEmacsライクなエディタlemを使ってみた

lemはCommon Lispで書かれたEmacsライクなエディタで、拡張もCommon Lispで書ける。cl-charmsというncursesのCFFIラッパーを使っている。

特に何もしてなくても起動が速いが、lemをロードした状態で処理系のコアイメージをダンプすることでさらに速くなる。

インストール

とのことなのでroswellさえ入っていれば導入は簡単。

ros install cxxxr/lem

こうすると、 ~/.roswell/local-projects/cxxxr/lem 以下にコードがダウンロードされ、Roswellの処理系でコンパイル、ロードされた後に処理系のコアイメージがダンプされる。さらにこのコアイメージを使って処理系を起動し、lemを起動するためのrosスクリプトが ~/.roswell/bin/lem にできる。 ~/.bashrc などでこれを起動できるようにパスを通しておく。

export PATH=$HOME/.roswell/bin:$PATH

あとはシェルからlemで起動できる。起動が速い!

設定ファイル .lemrc

設定は ~/.lemrc に書く。

当然ながらこの設定ファイルもCommon Lispで書ける。とりあえず普段Emacsで使っているキーバインドを設定してみる。

;;; -*- coding:utf-8; mode:Lisp  -*-

(in-package :lem)

(define-command split-3-window-horizontally () ()
  (let ((w-width (window-width)))
    (split-active-window-horizontally)
    (shrink-window-horizontally (- (window-width) (floor (/ w-width 3))))
    (other-window)
    (split-active-window-horizontally)
    (other-window)
    (other-window -2)))

;; Key Bindings
(define-key *global-keymap* "M-o" 'other-window)
(define-key *global-keymap* "M-i" 'delete-other-windows)
(define-key *global-keymap* "M-u" 'split-active-window-vertically)
(define-key *global-keymap* "M-U" 'split-3-window-horizontally)
(define-key *global-keymap* "M-z" 'query-replace)
(define-key *global-keymap* "C-x l" 'start-lisp-repl)

define-commandでコマンドを定義すると 'M-x コマンド名' でも呼べるしキーバインドを割り当てることもできる。自分がよく使う画面を縦に3分割するコマンド split-3-window-horizontally を定義してみた。コマンドの探し方はEmacsと同じように M-x apropos-command でキーワードを入れれば色々出てくる。

デフォルトでCommon Lispはシンタックスハイライトされるが、配色やスタイルをいつものやつにしたかったのでこれも設定する。

;;; syntax highlight
;; see (apropos "attribute")
(setf *enable-syntax-highlight* t)
(setf (attribute-fg-color *syntax-keyword-attribute*) "magenta")
(setf (attribute-bold-p *syntax-keyword-attribute*) t)
(setf (attribute-bold-p *syntax-function-name-attribute*) t)
(setf (attribute-fg-color *syntax-comment-attribute*) "cyan")
(setf (attribute-fg-color *syntax-constant-attribute*) "green")
(setf (attribute-fg-color *syntax-string-attribute*) "yellow")
(setf (attribute-fg-color *syntax-variable-attribute*) nil)
(setf (attribute-fg-color *mark-overlay-attribute*) nil)
(setf (attribute-reverse-p *mark-overlay-attribute*) t)

ソースコードをcolorでgrepしてみるとattribute構造体というのが出てきたので、REPLで apropos や describe などを使って設定できそうな変数を探した。その結果、シンタックスハイライトに関わる変数を見つけたので、これらの構造体のスロットを変更することで、前景色、背景色、ボールドにするか、反転するかなどを設定できることが分かった。

下はこの設定をした後の様子。

LispモードとREPL

Emacsと同様 C-x C-f で.lisp拡張子のファイルを開くとLispモードに入る。LispモードはS式単位の編集などは問題なくできた。 C-c C-j でEmacsの *scratch* のように式ごとの評価結果をバッファに書き込む形での評価ができるが、いつも式ごとの評価は C-x C-e でやっているのでその辺も設定しておく。

(in-package :lem.lisp-mode)

(define-key *lisp-mode-keymap* "C-x C-e" 'lisp-eval-last-sexp)
(define-key *lisp-mode-keymap* "C-c C-i" 'lisp-comment-region)
(define-key *lisp-mode-keymap* "C-c C-o" 'lisp-uncomment-region)

;; 現在は修正されている
;; (define-command lisp-repl-move-to-beginning-of-line () ()
;;   (move-to-beginning-of-line)
;;   (shift-position (+ (length (lisp-repl-get-prompt)) 2)))

;; (define-key *lisp-repl-mode-keymap* "C-a" 'lisp-repl-move-to-beginning-of-line)

Common LispのREPLは M-x start-lisp-repl で起動する。これはよく使うので C-x l に割り当てておく。

下はREPLを起動したところ。

defunの途中まで書くとちゃんとラムダリストを表示してくれるのが分かる。ただしこの状態でC-aするとプロンプトを突き抜けて行頭まで行ってしまうのでやっつけで lisp-repl-move-to-beginning-of-line を書いて C-a に割り当てた。(追記: これは現在は修正されている)

感想

lemは去年lispmeetupで@snmstsさんが紹介されていたので知ってはいたのだが、id:cxxxrさんによって地道に開発が続けられていて着実に進化していると思った。コードも読みやすいので拡張機能も書きやすいように思える。日本語入力はuim-fep経由でできるが、SKKクライアントの実装は簡単なので暇があればやってみたい。こうなってくるとiswitchbやelscreenのようなものも欲しくなってくる。

よくよく考えてみればlemに拡張機能を追加していった場合もlem自体をそうしたようにコンパイル、ロード済のコアイメージを読み込めばいいので起動は速いままなはずである。昨今のエディタは拡張機能を高級なスクリプト言語で書ける代わりに拡張が増えてくると起動が遅くなる場合が多い。そしてEmacsのemacsclient、vimの+clientserverのようにエディタを常駐化させるという苦肉の策に出る羽目になる。バイトコンパイルしたとしても結局VMにロードさせる時間はかかるので、ネイティブコンパイル済みのイメージをメモリに読み込むだけのlemが輝いて見える。

lemで一つ気になったのは、Lispで機械学習のような重い処理をやるとエディタも止まってしまって何もできなくなるということ。まあ多分そういう用途よりはさっと起動してちゃちゃっとスクリプトを書くような用途に向いているのかも。あるいはもう一つ別プロセスでLisp処理系を起ち上げてlem側でSLIMEのようなものを動かすという手もあるか。