M.Hiroi's Home Page

Common Lisp Programming

Common Lisp 入門 : 自作ライブラリ編

[ Home | Common Lisp ]

WHAT'S NEW


はじめに

今まで M.Hiroi が Common Lisp の勉強で作成したプログラムをライブラリにまとめたものです。ネットを探せばもっと優れたライブラリが見つかると思いますが、せっかく作ったプログラムを有効利用したかったので、あえてライブラリを作ってみることにしました。

自作ライブラリは ASDF というツールを使うと簡単に管理することができます。なお、今回作成するライブラリは quicklisp に対応しておりません。あしからずご了承くださいませ。

CONTENTS


●ASDF の簡単な使い方

ASDF (Another System Definition Facility) はライブラリのコンパイルやロードを行うためのツールです。多くの Common Lisp 処理系において、ASDF は標準で同梱されています。もちろん、SBCL にも ASDF は同梱されていて、(require :asdf) でロードすることができます。

* (require :asdf)
("ASDF" "asdf" "UIOP" "uiop")

* (asdf:asdf-version)
"3.3.1"

SBCL 2.1.11 に同梱されている ASDF のバージョンは 3.3.1 です。SBCL の設定ファイル ~/.sbclrc に (require :asdf) を書いておくと、SBCL の起動時に ASDF をロードすることができます。

ASDF では、ライブラリのことを「システム (system)」といいます。ASDF をロードすると、関数 require は ASDF のものに置き換えられます。そして、require を使ってシステムのコンパイルとロードを行うことができます。このとき、ASDF は標準で以下に示すパスから、指定されたシステムの定義ファイル (拡張子が .asd のファイル) を再帰的に探します。

たとえば、システム名が :sample とすると、上記パスの下に適当なサブディレクトリ (たとえば sample など) を作成し、その中にソースファイル sample.lisp と定義ファイル sample.asd を格納します。REPL で (require :sample) と入力すると、定義ファイル sample.asd に従って sample.lisp をコンパイルして、それをロードすることができます。

それでは実際に試してみましょう。最初にソースファイル sample.lisp にパッケージ sample を定義します。ASDF の場合、ソースファイルの拡張子は標準で .lisp を使います。

リスト : sample.lisp

(provide :sample)
(defpackage :sample (:use :cl))
(in-package :sample)
(export '(greeting))

(defun greeting ()
  (format t "hello, ASDF!~%"))

sample の中で関数 greeting を定義し、それを export しています。パッケージの説明は拙作のページ Common Lisp 入門: パッケージの基本的な使い方 をお読みください。

次に sample.asd でシステムを定義します。

リスト : sample.asd

(defsystem :sample
  :description "ASDF Sample Program"
  :version "0.0.1"
  :author "Makoto Hiroi"
  :license "MIT"
  :depends-on ()
  :components ((:file "sample")))

システムは defsystem で定義します。defsystem の後にシステム名を書き、そのあとにシステムの情報 (:description, :version, :author, :license など)、システムの依存関係 (:depends-on)、ソースファイル (:components ((:file "..."))) などを記述します。sample.lisp の場合、依存するシステムは無いので、ソースファイルを記述するだけで ASDF は動作しますが、システムを外部に公開するのであれば、他の情報も記述しておいたほうがいいでしょう。

実行例を示します。

$ sbcl
This is SBCL 2.1.11.debian,
・・・略・・・

* (require :sample)
; compiling file "/home/mhiroi/common-lisp/sample/sample.lisp" (written ... 略 ...):

; wrote /home/mhiroi/.cache/common-lisp/sbcl-2.1.11.debian-linux-x64/home/mhiroi/common-lisp/sample/
sample-tmpGHU3ALSV.fasl
; compilation finished in 0:00:00.010
("SAMPLE")

* (use-package :sample)
T

* (greeting)
hello, ASDF!
NIL

(require :sample) を評価すると、sample.lisp がコンパイルされて、fasl ファイルが ~/.cache/common-lisp/ 以下のサブディレクトリにキャッシュされます。ASDF はキャッシュに fasl ファイルがあればそれをロードします。

$ sbcl
This is SBCL 2.1.11.debian,
・・・略・・・

* (require :sample)
("SAMPLE")

fasl ファイルがシステムのソースファイルより古い場合、ASDF は自動的に再コンパイルしてキャッシュを更新します。

●簡易テストツール mintst の作成

Lisp / Scheme の場合、作成した関数は REPL で簡単にテストすることができますが、いちいち手動で入力するよりも、テストを自動で実行できると便利です。Common Lisp にはテストフレームワークがいくつもあり、その中では FiveAM や prove が有名なようです。

拙作のページ Common Lisp 入門では、関数の使用例として REPL での実行結果を示しています。今回はライブラリの使用例として、同じことを自動化できれば十分なので、簡単なテストツールを作ってみることにしました。以下にテストツールの仕様を示します。

簡単な実行例を示します。

* (require :mintst)
("MINTST")

* (use-package :mintst)
T

* (initial)
----- test start -----

0

* (run (+ 1 2) 3)
(+ 1 2)
=> 3 OK

NIL

* (run (+ 1 2) 4)
(+ 1 2)
=> 3 NG, 4

NIL

* (run (/ 1 (+)) nil)
(/ 1 (+))
=> ERROR: arithmetic error DIVISION-BY-ZERO signalled
Operation was (/ 1 0).

NIL

* (final)
----- test end -----
TEST: 3
OK: 1
NG: 1
ERR: 1
NIL

実際にはテストプログラムをファイルに書いて、それを ASDF の関数 asdf:test-system で実行します。次のリストを見てください。

;;;
;;; mintst_tst.lisp : mintst のテスト
;;;
;;; Copyright (c) 2023 Makoto Hiroi
;;;
;;; Released under the MIT license
;;; https://opensource.org/license/mit/
;;;
(provide :mintst_tst)
(defpackage :mintst_tst (:use :cl :mintst))
(in-package :mintst_tst)
(export '(test))

(defun test ()
  (initial)
  (run (+ 1 2) 3)
  (run (+ 1 2) 4)
  (run (/ 1 (+)) 0)
  (final))
リスト : mintst_tst.asd

(defsystem :mintst_tst
  :description "test for mintst"
  :version "0.1.0"
  :author "Makoto Hiroi"
  :license "MIT"
  :depends-on (:mintst)
  :components ((:file "mintst_tst"))
  :perform (test-op (o s)
                    (symbol-call :mintst_tst :test)))

テストプログラムはパッケージとして定義して、その asd ファイルも作成します。名前は :mintst_tst としました。:mintst_tst は :mintst を使っているので、defpackage の :use に :mintst を追加します。そして、asd ファイルの depends-on に :mintst を追加します。これで前もって ASDF が :mintst を読み込んでくれます。mintst_tst.lisp で :mintst を require する必要はありません。

次に :perform でアクション test-op を定義します。asdf:test-system を評価するとき、test-op に定義されている処理が実行されます。デフォルトでは何も設定されていません。symbol-call は ASDF (正確には ASDF が使用しているパッケージ UIOP) の関数で、パッケージに定義されている関数を呼び出します。第 1 引数にパッケージ、第 2 引数に関数を指定します。パッケージがロードされていなければ、それも行います。これで :mintst_tst の関数 test が呼び出されます。

最後に mintst.asd を修正します。

リスト : mintst.asd

(defsystem :mintst
  :description "minimum test tool"
  :version "0.1.0"
  :author "Makoto Hiroi"
  :license "MIT"
  :depends-on ()
  :in-order-to ((test-op (test-op :mintst_tst)))
  :components ((:file "mintst")))

:in-order-to はアクションの依存関係を定義します。上記の定義では、:mintst の test-op を実行するとき、:minitst_tst の test-op が実行されます。:mintst の test-op は何もしないので、(asdf:test-system :mintst) を評価すると、:mintst_tst の関数 foo が評価されてテストが実行されます。

それでは実際に試してみましょう。

* (asdf:test-system :mintst)
; compiling file ... 略 ...

----- test start -----

(+ 1 2)
=> 3 OK

(+ 1 2)
=> 3 NG, 4

(/ 1 (+))
=> ERROR: arithmetic error DIVISION-BY-ZERO signalled
Operation was (/ 1 0).

----- test end -----
TEST: 3
OK: 1
NG: 1
ERR: 1
T

正常に動作していますね。小さな自作ライブラリであれば、これだけでも十分役に立ちます。ASDF の詳しい説明は公式マニュアルや 参考 URL をお読みくださいませ。

●プログラムリスト

;;;
;;; mintst.lisp : 簡単なテストツール
;;;
;;; Copyright (c) 2023 Makoto Hiroi
;;;
;;; Released under the MIT license
;;; https://opensource.org/license/mit/
;;;
(provide :mintst)
(defpackage :mintst (:use :cl))
(in-package :mintst)
(export '(initial
          run
          final))

;;; 結果
(defvar *test-count* 0)
(defvar *ok-count* 0)
(defvar *ng-count* 0)
(defvar *err-count* 0)

;;; 初期化
(defun initial ()
  (format t "----- test start -----~%~%")
  (setf *test-count* 0
        *ok-count* 0
        *ng-count* 0
        *err-count* 0))

;;; result と collect が等しいかチェックする
(defun check (test expr collect)
  (when (null test) (setf test #'equal))
  (incf *test-count*)
  (format t "~a~%=> " expr)
  (handler-case
      (let ((result (eval expr)))
        (cond
         ((funcall test result collect)
          (incf *ok-count*)
          (format t "~a OK~%~%" result))
         (t
          (incf *ng-count*)
          (format t "~a NG, ~a~%~%" result collect))))
    (error (c)
           (incf *err-count*)
           (format t "ERROR: ~a~%~%" c))))

;;; テストの実行
(defmacro run (expr collect &key (test nil))
  `(mintst::check ,test  ',expr ,collect))

;;; 終了
(defun final ()
  (format t
          "----- test end -----~%TEST: ~d~%OK: ~d~%NG: ~d~%ERR: ~d~%"
          *test-count* *ok-count* *ng-count* *err-count*))

●参考 URL


権利・免責事項など

『お気楽 Common Lisp プログラミング入門 : 自作ライブラリ編』で作成したライブラリは『MIT ライセンス』で配布します。どうぞご自由にお使いくださいませ。

Copyright (C) 2023-2024 Makoto Hiroi
All rights reserved.

[ Home | Common Lisp ]