ECLでCommon Lispに入門する2 - EmacsとSLYとテスト
はじめに
前回の続きです。
Emacs拡張のSLYを使うことで、コンパイルや実行、テストが簡単にできるようになるので、SLYを使った開発手順と、テストの方法についてまとめます。
プロジェクトの雛形を作る
プロジェクトの雛形を作るためのスクリプトを用意したので、これをベースに進めます。
以下を適当な名前(psetup.sh)で、~/.local/binなどに保存します。
#!/bin/sh
PROJECT="$1"
if [ -z "$PROJECT" ]; then
echo "Usage: $0 <project-name>"
exit 1
fi
if [ -e "$PROJECT" ]; then
echo "Error: Directory '$PROJECT' already exists. Aborting."
exit 1
fi
mkdir -p "$PROJECT"/{src,tests,.deps}
# ASDファイル
cat > "$PROJECT/$PROJECT.asd" <<EOF
(defsystem "$PROJECT"
:version "0.1.0"
:author "Your Name"
:license "GPL"
:description "A Common Lisp project."
:depends-on ()
:pathname "src"
:serial t
:components ((:file "package")
(:file "main"))
:in-order-to ((test-op (test-op "$PROJECT/test"))))
(defsystem "$PROJECT/test"
:depends-on ("$PROJECT" "fiveam")
:pathname "tests"
:serial t
:components ((:file "main-test"))
:perform (test-op (o s)
(uiop:symbol-call :fiveam '#:run!
(uiop:find-symbol* '#:main-test-suite
:main-tests))))
EOF
# パッケージファイル
cat > "$PROJECT/src/package.lisp" <<EOF
(defpackage :$PROJECT
(:use :cl)
(:export :main))
EOF
# メインファイル
cat > "$PROJECT/src/main.lisp" <<EOF
(in-package :$PROJECT)
(defun main ()
(format t "Hello from $PROJECT!~%"))
EOF
# テストファイル
cat > "$PROJECT/tests/main-test.lisp" <<EOF
(defpackage :main-tests
(:use :cl
:fiveam))
(in-package :main-tests)
(def-suite main-test-suite)
(in-suite main-test-suite)
(test test-fn
;; テストを書きます
)
EOF
mkdir -p "$PROJECT/.deps/cache" # コンパイル済みキャッシュ
QL_DIR="$PROJECT/.deps/systems"
mkdir -p "$QL_DIR"
cd "$PROJECT"
echo "Downloading Quicklisp"
curl -O https://beta.quicklisp.org/quicklisp.lisp
echo "Installing Quicklisp to $QL_DIR"
LISP="ecl" # 処理系でここ変更する
"$LISP" --load quicklisp.lisp \
--eval "(quicklisp-quickstart:install :path \".deps/systems\")" \
--eval "(quit)"
rm quicklisp.lisp
echo "Quicklisp installed to $QL_DIR"
echo "Project '$PROJECT' created successfully."
続いて、Quicklisp経由の操作をシェルから簡易的に行うため、以下のシェルスクリプトをql.shのような名前で~/.local/binに保存します。
#!/bin/sh
COMMAND="$1"
SYSTEM="$2"
QUICKLISP_SETUP="$(pwd)/.deps/systems/setup.lisp"
if [ ! -f "$QUICKLISP_SETUP" ]; then
echo "Quicklisp setup.lisp not found at $QUICKLISP_SETUP"
exit 1
fi
if [ -z "$COMMAND" ] || [ -z "$SYSTEM" ]; then
echo "Usage: $0 {install|uninstall|search} <system-name>"
exit 1
fi
case "$COMMAND" in
install)
ACTION="(ql:quickload :$SYSTEM)"
;;
uninstall)
ACTION="(ql:uninstall :$SYSTEM)"
;;
search)
ACTION="(progn
(format t \"~%Results for ~A:~%~%\" \"$SYSTEM\")
(loop for name in (ql:system-apropos \"$SYSTEM\") do (format t \"~A~%\" name)))"
;;
*)
echo "Unknown command: $COMMAND"
exit 1
;;
esac
ecl --eval "(load \"$QUICKLISP_SETUP\")" \
--eval "$ACTION" \
--eval "(quit)"
ここまで完了したら、
$ cd ~/Project
$ psetup.sh picopico
のような感じで実行すると、Quicklispのセットアップも行うため少し時間がかかりますが、プロジェクトディレクトリとプロジェクトが作成されます。プロジェクトは以下のような構成になります。
$ cd picopico
$ tree
.
├── picopico.asd
├── src
│ ├── main.lisp
│ └── package.lisp
└── tests
└── main-test.lisp
3 directories, 4 files
picopico/.depsには、Quicklisp本体や、インストールしたシステム、コンパイル済みのキャッシュが格納されます。
EmacsでSLYを設定する
~/.config/emacs.d/init.elに以下のコードを記述します。
(use-package sly
:straight t
:init
(setq inferior-lisp-program "ecl") ; 使用する処理系を指定
:hook (lisp-mode . sly-editing-mode)
:config
(setq sly-contribs '(sly-mrepl))
(setq sly-lisp-implementations
'((ecl ("ecl"))
;; 複数処理系を切り替えるならここに追加
)))
SLYについて
SLY User Manualにマニュアルがあります。Common Lispのインタラクティブな開発環境(IDE)をEmacsに追加する拡張です。
EmacsでSLYを起動し、プロジェクトを開く
作成したpicopicoというプロジェクトをEmacsで開きます。
$ cd ~/Project/picopico
$ emacs picopico.asd &
起動後、M-x sly を実行するとSLYが起動します。初回はSLY自体のコンパイルが行われるため、少し時間がかかります。コンパイルキャッシュはASDFの設定に基づくため、前回記載のASDFの設定を.eclrcに記述している場合は、picopico/.deps/cache以下に生成されます。
無事起動すると、開いたasdファイルに加え、SLYのREPL環境が分割されて表示されます。

*Ibuffer*を確認すると、以下のように複数のSLY関連のバッファを確認できます。
*sly-mrepl for ecl**sly-inferior-lisp for ecl**sly-events for ecl*
このうち、sly-inferior-lispは標準エラーなどの出力、sly-eventsはSLYとLISP間のイベント周りの出力となり、開発で常用するのはsly-mreplバッファになります。
ソースコードのコンパイル
全体のコンパイル
まずは今あるpackage.lispとmain.lispをコンパイルします。asdf:load-systemをmreplで実行すると、プロジェクト全体でコンパイルが実行されます。mreplへの移動は、C-x oでフレームを移動しても良いですが、C-c C-zで移動できます。
CL-USER> (asdf:load-system :picopico)
;;;
;;; Compiling /home/hiro/Projects/picopico/src/package.lisp.
;;; OPTIMIZE levels: Safety=2, Space=0, Speed=3, Debug=3
;;;
;;; Finished compiling /home/hiro/Projects/picopico/src/package.lisp.
;;;
;;;
;;; Compiling /home/hiro/Projects/picopico/src/main.lisp.
;;; OPTIMIZE levels: Safety=2, Space=0, Speed=3, Debug=3
;;;
;;; Finished compiling /home/hiro/Projects/picopico/src/main.lisp.
;;;
T
システム名と呼び出す関数を指定し、関数を実行できることを確認します。
CL-USER> (picopico:main)
Hello from picopico!
NIL
ファイル単位のコンパイル
次は、ファイル単位のコンパイルを試します。C-x C-fでsrc/main.lispを開きます。
(in-package :picopico)
(defun main ()
(format t "Hello from picopico!~%")
(format t "Goodbye~%"))
Goodbyeという文字を表示する処理を追加し、C-c C-kを実行すると、そのファイルをコンパイルします。正常にコンパイルされると、以下のような表示がmrepl側に表示されます。
;;;
;;; Compiling /home/hiro/Projects/picopico/src/main.lisp.
;;; OPTIMIZE levels: Safety=2, Space=0, Speed=3, Debug=3
;;;
;;; Finished compiling /home/hiro/Projects/picopico/src/main.lisp.
;;;
;;;
;;; Compiling /home/hiro/Projects/picopico/src/main.lisp.
;;; OPTIMIZE levels: Safety=2, Space=0, Speed=3, Debug=3
;;;
;;; Finished compiling /home/hiro/Projects/picopico/src/main.lisp.
;;;
以下のように、わざと間違えた関数呼び出しを行った場合などはコンパイルエラーが発生します。
(defun main ()
(format t "Hello from picopico!~%")
(format "Goodbye~%"))
コンパイルエラーは、sly-compilationというバッファに以下のように出力されます。FORMAT関数の引数がおかしいということがエラー内容に表示されています。
cd /home/hiro/Projects/picopico/src/
3 compiler notes:
main.lisp:3:1:
style-warning:
Warning:
in file main.lisp, position 22
at (DEFUN MAIN ...)
! Too few arguments for proclaimed function FORMAT
style-warning:
Warning:
in file main.lisp, position 22
at (DEFUN MAIN ...)
! Too few arguments for proclaimed function FORMAT
error:
Error:
in file main.lisp, position 22
at (DEFUN MAIN ...)
* Wrong number of arguments for function FORMAT
Compilation failed.
sly-compilationにはエラーの他、コンパイル時の警告なども表示されます。C-c C-kでEmacsのステータスにWarningが表示された場合には、sly-compilationを確認してください。
Goodbyeを追加しコンパイルした後、先ほどと同様main関数を呼び出すと、以下のように結果が変わります。
CL-USER> (picopico:main)
Hello from picopico!
Goodbye
NIL
関数単位のコンパイル
関数単位のコンパイルを確認するため、add1とadd2の関数を作成し、その結果を確認できるようにします。
(in-package :picopico)
(defun add1()
(+ 1 1))
(defun add2()
(+ 1 2))
(defun main ()
(format t "~D,~D~%" (add1) (add2))
(format t "Hello from picopico!~%")
(format t "Goodbye~%"))
最初にC-c C-kでファイル全体をコンパイルし、実行結果が以下のようになることを確認します。
CL-USER> (picopico:main)
2,3
Hello from picopico!
Goodbye
NIL
次に、add1とadd2の関数の結果が変わるように書き換えます。
(defun add1()
(+ 1 10))
(defun add2()
(+ 1 20))
このままC-c C-kを実行してしまうとファイル全体がコンパイルされますが、add2だけをコンパイルするためカーソルをadd2に移動させてC-c C-cを実行します。再度プログラムを実行すると、add1の変更はコンパイルされていないため、add1の結果は2、add2の結果は21になります。
CL-USER> (picopico:main)
2,21
Hello from picopico!
Goodbye
NIL
関数単位のコンパイルは、継続的に動作し続けるプログラムの動作中、一部の関数の動作を変更して確認したいと言ったときに、停止することなく関数の動作を変更できるという利点があります。
テスト
FiveAMというシステムを使うことでテストを行うことができます。ここでは、
CL-USER> (asdf:test-system :picopico)
と入力した際、picopicoのテストが実行されるようにします。
asdファイルの確認
picopico.asdのpicopico/testというシステムはテストのためのものです。
(defsystem "picopico/test"
:depends-on ("picopico" "fiveam")
:pathname "tests"
:serial t
:components ((:file "main-test"))
:perform (test-op (o s)
(uiop:symbol-call :fiveam '#:run!
(uiop:find-symbol* '#:main-test-suite
:main-tests))))
-
:depends-on依存するシステム -
:pathnameテストコードのディレクトリ -
:performtest-systemを実行する際、fiveam:run! に、main-tests:main-test-suiteを引数として渡して実行する設定
(asdf:test-system :picopico) を実行した際、picopico/testをテストとして実行させる設定は、以下のin-order-toの部分で記述します。
(defsystem "picopico"
;; 省略
:in-order-to ((test-op (test-op "picopico/test"))))
依存システムのインストール
プロジェクトのトップで、picopico/testが依存しているシステムをインストールするため、以下のコマンドを実行します。
$ cd ~/Project/picopico
$ ql.sh install "picopico/test"
テスト対象の関数を作る
add1という関数を新しく作ります。src/package.lispのexportにまずは関数を追加します。
(defpackage :picopico
(:use :cl)
(:export :main :add1))
src/main.lispに関数を追加します。
(in-package :picopico)
(defun add1(n)
(+ n 1))
(defun main ()
(format t "Hello from picopico!~%"))
テストを書く
tests/main-test.lispというファイルを開き、以下のようにadd1をテストするための、test-add1というテストを記述します。もし外部に公開していない関数をテストする場合にはpicopico::add1というようにコロンを2つ繋げて記述します。
(defpackage :main-tests
(:use :cl
:fiveam))
(in-package :main-tests)
(def-suite main-test-suite)
(in-suite main-test-suite)
(test test-add1
(is (= 2 (picopico:add1 1))))
(asdf:test-system :picopico)でテストが実行されるかを確認します。
CL-USER> (asdf:test-system :picopico)
;;;
;;; Compiling /home/hiro/Projects/picopico/src/package.lisp.
;;; OPTIMIZE levels: Safety=2, Space=0, Speed=3, Debug=3
;;;
;;; Finished compiling /home/hiro/Projects/picopico/src/package.lisp.
;;;
;;;
;;; Compiling /home/hiro/Projects/picopico/src/main.lisp.
;;; OPTIMIZE levels: Safety=2, Space=0, Speed=3, Debug=3
;;;
;;; Finished compiling /home/hiro/Projects/picopico/src/main.lisp.
;;;
;;;
;;; Compiling /home/hiro/Projects/picopico/tests/main-test.lisp.
;;; OPTIMIZE levels: Safety=2, Space=0, Speed=3, Debug=3
;;;
;;; Finished compiling /home/hiro/Projects/picopico/tests/main-test.lisp.
;;;
Running test suite MAIN-TEST-SUITE
Running test TEST-ADD1 .
Did 1 check.
Pass: 1 (100%)
Skip: 0 ( 0%)
Fail: 0 ( 0%)
T
Discussion