7/19/2011

第6回 Common Lispライブラリを書く

 僕のブログで「Modern Common Lispはターゲットをどこに置いているのか」という質問をいただきました。最初は環境構築から始まっており、初心者向けに見えますが、次はHello, Worldもせずにライブラリの解説をしています。

 Modern Common Lispのターゲットは、これからのCommon Lispを学びたい人です。当初はこの連載を読めばCommon Lispについてのすべてを学べるAll-In-One形式にしようと考えていたのですが、それでは時間がかかりすぎてしまうため、いくつかの過程を省いて進めます。たとえば、Lispでは必要なリスト処理などはここでは解説しません。理由は、それを学ぶための書籍が既に多く存在するからです。Common Lispの基本文法はそれらの書籍を参照してください。この連載は、Common Lispの入門書と並行、または読了後に読むことをおすすめします。

 さて、前回の僕の記事では主要なライブラリとその使い方について紹介しました。今回は自分のCommon Lispのライブラリを書く手順についてです。

Sleep Sortのライブラリをつくろう

 1ヶ月以上前のことになりますが、Sleep Sortというソートアルゴリズムが話題になりました。

 実装が簡単なので、例として今回はこのライブラリを作りたいと思います。

CL-Projectでスケルトン生成

 まずはライブラリの雛形を作ります。雛形の生成にはCL-Projectというライブラリを使います。まずはQuicklispでインストールしましょう。

(ql:quickload :cl-project)

 雛形を作るには関数make-projectを使います。あらかじめ依存することが分かっているライブラリはこのタイミングで指定しておきましょう。今回はcl-annot(後述)と、スレッドを使いそうなのでbordeaux-threadsの2つを指定しています。

(cl-project:make-project #p"lib/sleepsort/"
  :author "Eitarow Fukamachi"
  :email "e.arrows@gmail.com"
  :license "LLGPL"
  :depends-on '(:bordeaux-threads cl-annot))
;-> writing /Users/fukamachi/Programs/lib/sleepsort/.gitignore
    writing /Users/fukamachi/Programs/lib/sleepsort/README.markdown
    writing /Users/fukamachi/Programs/lib/sleepsort/sleepsort-test.asd
    writing /Users/fukamachi/Programs/lib/sleepsort/sleepsort.asd
    writing /Users/fukamachi/Programs/lib/sleepsort/src/sleepsort.lisp
    writing /Users/fukamachi/Programs/lib/sleepsort/t/sleepsort.lisp
;=> #P"/Users/fukamachi/Programs/lib/sleepsort/sleepsort.asd"

 これで#p"lib/sleepsort/"にスケルトンができました。空のプロジェクトはそのままの状態でもql:quickloadすることができます。

(ql:quickload :sleepsort)
;-> To load "sleepsort":
      Load 1 ASDF system:
        sleepsort
    ; Loading "sleepsort"
    [package sleepsort]
;=> (:SLEEPSORT)

※注) もし#p"lib/sleepsort/"にパスが通っていない場合、REPLを再起動するとロードできなくなります。ローカルのプロジェクトをQuicklispでロードできるようにするには第2回 Quicklispによるライブラリ環境を参照してください。

雛形の構成

 CL-Projectが生成する雛形は以下のような最小構成です。

  • README.markdown: README。
  • sleepsort.asd: ライブラリ(system)の定義ファイル。
  • sleepsort-test.asd: ライブラリのテスト用の定義ファイル。
  • src/
    • sleepsort.lisp: メインのソースファイル。
  • t/
    • sleepsort.lisp: メインのテストファイル。

 ディレクトリトップには.asdファイルが2つあります。これは他の言語で言うところのMakefileです。ASDFによるライブラリの定義ファイルです。Quicklispはこのファイルから必要なファイルを判断してライブラリをロードしています。

 srcディレクトリは主なソースコードを置く場所です。この中には最初1つのファイルが置かれていますが、他にファイルを分けたいと思ったときにはこのディレクトリにファイルをどんどん追加していきます。

 tディレクトリは自動テストを置く場所です。

関数を実装

 では、肝心の関数の定義をします。src/sleepsort.lispを開いてください。以下のようになっているはずです。

#|
  This file is a part of sleepsort project.
  Copyright (c) 2011 Eitarow Fukamachi (e.arrows@gmail.com)
|#

(in-package :cl-user)
(defpackage sleepsort
  (:use :cl))
(in-package :sleepsort)

;; blah blah blah.

 これにsleepsortという関数を追加します。

#|
  This file is a part of sleepsort project.
  Copyright (c) 2011 Eitarow Fukamachi (e.arrows@gmail.com)
|#

(in-package :cl-user)
(defpackage sleepsort
  (:use :cl)
  (:import-from :bordeaux-threads
                :make-thread
                :join-thread))
(in-package :sleepsort)

(cl-annot:enable-annot-syntax)

@export
(defun sleepsort (&rest args)
  "A function to do 'sleep sort' arguments."
  (let* (result
         (threads (mapcar (lambda (arg)
                            (bordeaux-threads:make-thread
                             (lambda ()
                               (sleep arg)
                               (push arg result))))
                          args)))

    (dolist (thread threads)
      (bordeaux-threads:join-thread thread))

    (nreverse result)))

 @exportアノテーションを有効にするためにcl-annotを使っています。このアノテーションを使うと、指定した関数がパッケージ外でも参照できるようになります。

 cl-annotの詳しい解説は、作者の松山が今後紹介記事を書いてくれる予定です。

テストを書く

 モダンなライブラリに自動テストは欠かせません。雛形にはテスト用のファイルも含まれています。t/sleepsort.lispを開いてください。

#|
  This file is a part of sleepsort project.
  Copyright (c) 2011 Eitarow Fukamachi (e.arrows@gmail.com)
|#

(in-package :cl-user)
(defpackage sleepsort-test
  (:use :cl
        :sleepsort
        :cl-test-more))
(in-package :sleepsort-test)

(plan nil)

;; blah blah blah.

(finalize)

 この"blah blah blah."の部分を消して、以下のように置き換えてみます。

(diag "Testing 'sleepsort'. Wait some seconds...")
(is (sleepsort 5 3 6 3 6 3 1 4 7) '(1 3 3 3 4 5 6 6 7))

 テストを走らせるには(test-system :sleepsort)を実行します。

(test-system :sleepsort)
To load "sleepsort-test":
  Load 1 ASDF system:
    sleepsort-test
; Loading "sleepsort-test"
[package sleepsort-test]
# Testing 'sleepsort'. Wait some seconds...
ok 1

(:SLEEPSORT-TEST)

 Sleep Sortの実行は、その性質上時間がかかります。7秒ほど待ったあと、"ok"と表示されればテスト成功です。

 詳しいCL-TEST-MOREの説明はhttps://github.com/fukamachi/cl-test-moreを参照してください。

Quicklispに登録する

 せっかく作った有用なライブラリはQuicklispに登録すべきです。現在Quicklispへの登録は承認制になっており、月に一度の更新時に取り込まれます。日にちは固定ではないですが、だいたい毎月20日前後に更新されます。

 登録の申請はGitHub Issuesで行います。

 右上の「New Issue」をクリックし、ライブラリ名とリポジトリのURLを書いて登録してください。

おわりに

 さて、今回は自分のライブラリを作って公開するまでを紹介しました。公開するためでなくても、何かアプリケーションを作るときはasdファイルを書いてモジュール単位で管理するため、Common Lispでまとまったプログラムを書くときには必要な手順でしょう。

 次回の僕の回ではCL-TEST-MOREでの自動テストについてより詳しく解説します。

7/10/2011

第5回 SLIMEの使い方 開発サイクルについて

前々回の「基礎編」 ではSLIMEのインストール方法とその基本的な使い方について説明しましたが、 具体的にどのように開発したら良いか分からないと思います。今回はその補足 としてSLIMEにおける開発サイクルについて説明します。

SLIMEにおける開発サイクル

SLIMEにおける開発サイクルは基本的には次のようになります。

  1. M-x slimeでSLIMEを起動
  2. lispファイルを編集
  3. REPLバッファで動作確認
  4. 2に戻る

一般的な開発サイクルには「コンパイル」とか「アプリケーションの再起動」 というタスクがありますが、SLIMEの開発サイクルにはそれがありません。その 代わりSLIMEには、アプリケーションへの変更を即座に反映させる仕組みがあり ます。これにより、待ち時間を極力短縮して、またエディタとターミナルを行 き来することなく、効率的に(また気持ち良く)開発することが可能になりま す。この、いわゆるインクリメンタルなアプリケーションの構築は、SLIMEを使 う最大の利点と言っても過言ではありません。

もっとも、(2)の「lispファイルを編集」するための十分な知識を持っていなけ れば、その利点もあまり感じられないでしょう。本連載でもこの点を重点的に 解説していく予定です。本エントリの後半もその解説になります。

Webアプリケーションなどを開発する際は、(3)は「ブラウザで動作確認」にな ります。もちろんユーティリティ関数のテストやモデルデータの確認をREPLで 行うこともあります。要は使い分けです。

ところで、アプリケーションをインクリメンタルに構築していくという性質上、 何かの拍子に変数の値がおかしくなったり、謎のエラー(というわけではない ですが、原因を考えるのが本質的でないエラー)が発生したりします。そうい う時は、M-x slime-restart-inferior-lispとして開発サイクルを一度リセッ トします。この操作は重要なので是非覚えてください。

バッファでの操作

ここでの操作は上記した開発サイクルの(2)に該当します。SLIMEで一番重要な 操作ですので、是非習得してください。

C-c C-c (slime-compile-defun)

このコマンドは現在ポイントしているトップレベル関数(実は関数以外でも有 効)をコンパイルするコマンドです。コンパイルというと少しヘビーなイメー ジを持ちますが、ここでは関数を定義するぐらいのイメージで問題ありません。 実際に例を示します。まず適当なlispファイルに次のコードを書いてくださ い。

(defun fact (n)
  (if (<= n 1)
      0
      (* n (fact (1- n)))))

次にこのフォームをポイントしてC-c C-cします。コンパイルが成功したとい うメッセージがミニバッファに表示されるはずです。これでfact関数が定義 され利用できるようになりました。

C-c C-zあるいはM-x slime-replとしてREPLバッファを表示し、次のコード を実行してみましょう。

CL-USER> (fact 10)
0

結果が0になっていて、どうもfact関数にバグがあるようです。実はnが1以 下のときに1ではなく0を返してしまっていさう。そこで再びlispファイル を開いて次のように修正してください。

(defun fact (n)
  (if (<= n 1)
      1
      (* n (fact (1- n)))))

そして、先と同じようにC-c C-cします。関数が定義できたらC-c C-zある いはM-x slime-replでREPLバッファを表示し、先と同じコードを実行してみ ましょう。

CL-USER> (fact 10)
3628800

正しい結果が返ってきていますね。この例でも分かるように、バッファでの操 作で一番重要なコマンドはC-c C-cです。関数定義(もっと言えばトップレベ ルフォーム)を編集した際は、C-c C-cする癖を付けるのが良いでしょう。

また、C-c C-cすることで、コンパイラがおかしなコードを拒否したり警告し てくれます。これはバグの早期発見に有効です。試しに次のようなコードを書 いてC-c C-cしてみてください。

(defun f (n)
  (let ((m (* n 2)))
    (1+ n)))

するとlet部分に下線が引かれると思います。この下線は警告を表わしており、 C-x \``あるいはM-x next-error`で警告の内容を詳細表示できます。

cd /home/tomo/tmp/
1 compiler notes:

/home/tomo/tmp/a.lisp:2:3:
  style-warning: Unused lexical variable M

このようにmという変数が未使用であるのが分かりますね。

余力がある方は、明らかにエラーとなるコードを書いてみて、C-c C-cしてみ ることをおすすめします。

C-c C-k (slime-compile-and-load-file)

C-c C-cほどではありませんが、それでも頻繁に使うのがC-c C-kです。こ のコマンドはファイル全体をコンパイルしてロードします。「コンパイル」と か「ロード」はCommon Lispの用語ですが、ここではあまり気にせず、ファイル を先頭から読み直すコマンド、ぐらいのイメージで捉えてください。

C-c C-kは基本的には、まだロードされていないファイルをロードするときに 使います。このとき、C-c C-c同様、コンパイルできないコードはエラーや警 告として視覚的に表示され、C-x \``あるいはM-x next-error`で順番に確認 することができます。

またC-c C-kは、C-c C-cし忘れていないことを確定するのにも便利です。 その他にも対応していない括弧などを検出するのにも使えます。

一般的に、何かおかしくなったらC-c C-kしてみる、それでも駄目なら上記し たM-x slime-restart-inferior-lispする、という運用になります。

C-c C-z (slime-repl)

すでに登場しましたが、このコマンドはREPLバッファを表示します。REPLバッ ファは頻繁に使うことになるので、是非覚えてください。

M-. (slime-edit-definition)

このコマンドは現在ポイントしているシンボルの定義を探すコマンドです。コー ディング時はもちろんのことながら、コードリーディング時やデバッグ時にも 非常に有用です。次に説明するM-,と対で覚えてください。

M-, (slime-pop-find-definition-stack)

このコマンドはM-.を実行したときの場所に戻るコマンドです。M-.で定義 を確認した後に、M-,で元の場所に戻るというのが一般的な使い方です。

REPLでの操作

「バッファでの操作」で説明した操作はREPLでも有効ですが、いくつか追加機 能が用意されているので、それを紹介します。

M-p, M-n

M-p, M-nはそれぞれ前の履歴、次の履歴を補完します。最低限これだけは 記憶してください。同じ式を何度も入力するのはよしましょう。

M-r

このコマンドは正規表現で履歴を検索して補完します。目的の履歴が最初に補 完されない場合は、M-p, M-nで順番に探します。インクリメンタルな検索 が出来れば便利ですが、現状ではそのような機能はないようです。誰か anythingで実装してくれないかな。

特殊な変数

REPL(Top Level Loop)では*, **, ***という特殊な変数が利用できま す。*には前回評価した式の結果が入り、**には前々回、***には前々々 回に評価した式の結果が入ります。評価結果を再利用するときに便利です。簡 単な例を示しておきます。

CL-USER> (+ 1 2)
3
CL-USER> (* * *)
9
CL-USER> (- * **)
6

二番目の式の最初の*は変数参照じゃなくて関数適用です。誤解しないように。 よく分からない人はCommon Lispの入門ページなどで勉強してください。

まとめ

とりあえず今回はここまでです。次回以降は実際に何か作りながら、より実践的な 内容について解説したいと思います。