Common Lisp

目次

主要項目のみを表示しています。詳細な小見出しは本文内で確認できます。

概要

まず、この章の中心構造を図で確認します。細部に入る前に、どの概念がどこへつながるかをつかむための地図です。

flowchart LR A["S式"] --> B["リーダ"] B --> C["評価器"] C --> D["関数"] C --> E["マクロ"] E --> F["コードを生成"] D --> G["実行"] F --> C C --> H["REPL"] H --> A
コード例の読み方

コード例は、そのまま写すためだけのものではありません。直前の本文で「何を確かめる例か」を押さえ、直後の説明で「どの性質が見えるか」を確認してください。実務では、ここに入力の境界、失敗時の挙動、依存する実行環境を足して読むと判断しやすくなります。

要点

Common Lispは1984年に誕生した汎用プログラミング言語で、強力なマクロシステム、CLOS(多重ディスパッチOOP)、コンディションシステム、REPL駆動開発を特徴とします。「言語そのものを拡張できる言語」として、AI研究や処理系の自身の実装で使われ続けています。

このページでは、S式、マクロ、CLOS、コンディション、REPL駆動開発、SBCLQuicklisp、ASDFを中心に整理します。


1. Common Lispとは何か・なぜ生まれたか

このセクションでは「Common Lispがなぜ生まれたのか」「なぜ40年経っても独自の地位を保つのか」「Lispファミリーとの関係」を整理します。

Common Lispは 「Lisp方言の集大成・標準化版」。1984年に複数のLisp方言を統一する目的で設計され、1994年にANSI標準化されました。

Common Lisp = マクロ + CLOS + コンディション + REPL + 動的型付け + 静的型注釈

1-1. Lispの歴史

1958  John McCarthyがLispを発明(MIT、世界2番目の高水準言語)
1960論文「Recursive Functions of Symbolic Expressions」
1962  LISP 1.5
1980年代 多数の方言が乱立: MacLisp, InterLisp, Scheme, ZetaLisp, Common Lisp...
1984  Common Lisp初版仕様書("Common Lisp the Language")
1994  ANSI標準(X3.226-1994、INCITS)

Lispは 「世界で2番目に古い高水準言語」(1番目はFORTRAN)。それなのに 今でも現役で使われ続けている稀有な言語です。


1-2. なぜLispは特別か

Lispの最大の特徴は 「コードがデータである(Code as Data)」。この性質が他言語にはない柔軟性を生みます。

flowchart LR A["S式を読む"] --> B["リストとして保持"] B --> C{"マクロ展開が必要?"} C -->|はい| D["コードを変換"] D --> E["評価"] C -->|いいえ| E E --> F["値を返す"] F --> G["REPLで確認"] G --> A

Common Lispでは、プログラムが一度テキストから構文木へ変換されて終わるのではなく、S式というデータ構造として扱えます。マクロはこの「評価前のコード」を受け取り、別のコードへ変換する仕組みです。

;; これはLispのコード
(+ 1 2 3)

;; 同時に、これはLispのリスト(データ)
'(+ 1 2 3)

コード = リスト」なので、コード自身を 値として操作・変換できます。これがマクロの基盤。

Greenspun’s Tenth Rule

“Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp.”

── Philip Greenspun

「十分に複雑なCやFORTRANのプログラムは、Common Lispの半分を即席で・仕様もなく・バグだらけに・遅く実装している」という有名な格言。


1-3. 設計目標

Common Lispの主な目標:

  1. 方言統合: MacLisp、InterLisp、Lisp Machine Lispなどを統一
  2. 強力な抽象化: マクロで言語自体を拡張可能
  3. 対話的開発: REPLでの動的開発を中心に
  4. 大規模対応: 産業・研究で使えるパッケージシステム
  5. 多パラダイム: 関数型 + OOP + 命令型を統合

1-4. Common Lispが動く場所

歴史的: AI研究(MIT、CMU)、Symbolics社のLisp Machine
現代:   ITA Software(航空券予約、Googleが買収)
        Grammarly(検索基盤の一部)
        AWSのMAA(Multi-Agent Architecture)
        ROS(ロボットOS、一部)
        理論研究、数学処理系(Maxima、Axiom)
        個人開発、ホビープロジェクト

メインストリームから少し離れた、しかし根強いコミュニティ」を持つ言語です。


1-5. このセクションのまとめ

- 1958年McCarthyが発明(世界2番目の高水準言語)
- 1984年Common Lisp、1994年ANSI標準
- 「コードがデータ」がLispの核心
- マクロ、CLOS、コンディション、REPL駆動
- 産業利用は限定的だが根強いコミュニティ

2. 環境構築(SBCL / Roswell / Quicklisp)

2-1. 主要処理系

SBCL(Steel Bank Common Lisp)  最も人気、高速、推奨
CCL(Clozure CL)                Mac/Windowsで速い
ECL(Embeddable CL)             組み込み向け、C統合
ABCL(Armed Bear CL)            JVM上
CLISP移植性重視
LispWorks商用、IDE充実
Allegro CL商用、エンタープライズ

新規学習者には SBCL推奨。最も活発、最速、無料。


2-2. インストール

macOS

brew install sbcl
sbcl --version

Linux

apt install sbcl       # Ubuntu/Debian
dnf install sbcl       # Fedora

Windows

SBCL公式からバイナリ


2-3. Roswell(推奨)

複数Lisp実装を管理するツール。Rubyのrbenv、Nodeのnvmに相当。

# macOS
brew install roswell

# Linux
curl -L https://raw.githubusercontent.com/roswell/roswell/master/scripts/install-for-ci.sh | sh

# 使用
ros install sbcl-bin
ros use sbcl-bin
ros run                # REPL起動

2-4. Quicklisp(パッケージマネージャ)

curl -O https://beta.quicklisp.org/quicklisp.lisp
sbcl --load quicklisp.lisp

;; REPL内で:
(quicklisp-quickstart:install)
(ql:add-to-init-file)
;; ライブラリのインストールと読み込み
(ql:quickload :alexandria)
(ql:quickload :hunchentoot)    ; Webサーバ

2-5. エディタ:Emacs + SLIME

Common Lisp開発の 事実上の標準

# SLIME(Emacs内IDE)
M-x package-install RET slime RET

;; .emacsに
(setq inferior-lisp-program "sbcl")
(slime)         ;; M-x slimeで起動

主なキーバインド:

C-c C-c関数定義をコンパイル
C-c C-kバッファ全体をコンパイル
C-x C-e式を評価
C-c C-d C-dシンボルの説明(HyperSpec)
M-.        定義へジャンプ

モダンな代替

- Sly(SLIMEのfork、より新しい)
- Lem(Lisp製の現代的エディタ)
- VSCode + Alive拡張
- Atom + Slima

2-6. 簡単なREPLセッション

* (+ 1 2 3)
6

* (defun greet (name) (format nil "Hello, ~a!" name))
GREET

* (greet "World")
"Hello, World!"

* (loop for i from 1 to 5 collect (* i i))
(1 4 9 16 25)

2-7. このセクションのまとめ

- SBCLが現代の標準処理系
- Roswellでバージョン管理
- Quicklispでライブラリ管理
- Emacs + SLIMEが伝統、Sly / Lemも選択肢
- REPL駆動がLisp開発の核心

3. S式とリーダ

3-1. S式(Symbolic Expression)

Lispのすべての構文は S式(symbolic expression)で表現されます。

S式 = アトム または リスト
アトム: 数値 / 文字列 / シンボル
リスト: (S式S式 ...)
42                    ; 数値(アトム)
"hello"               ; 文字列(アトム)
foo                   ; シンボル(アトム)
(1 2 3)               ; リスト
(+ 1 2)               ; リスト(関数呼び出し)
(if (> x 0) "pos" "neg")

3-2. 関数呼び出し

;; 関数名と引数すべて括弧の中
(関数名 引数1引数2 ...)

(+ 1 2 3)           ;; 6
(* 2 3)             ;; 6
(- 10 3)            ;; 7
(/ 10 3)            ;; 10/3(有理数!)
(mod 10 3)          ;; 1
(expt 2 10)         ;; 1024

前置記法(prefix notation)」。すべての操作が (operator args...) という統一形式。

引数が任意個数

(+ 1 2 3 4 5)       ;; 15
(* )                 ;; 1(積の単位元)
(+ )                 ;; 0

3-3. クォート

'(1 2 3)            ;; '(1 2 3) :リテラルとしてのリスト
(quote (1 2 3))     ;; 同等

(1 2 3)             ;; エラー!1を関数として呼ぼうとする

'expr(quote expr) の糖衣。「評価せずにそのまま」を意味する。


3-4. リーダ

ソースコードを トークン → S式に変換するのが「リーダ」。read 関数でREPLから取得できる:

* (read)
(+ 1 2)            ;; ユーザ入力
(+ 1 2)

リーダは マクロ文字('、`、,、#) の処理も担う(後述)。


3-5. コメント

; 行コメント
;; 慣習的に2つ以上
;;; トップレベル

#| ブロックコメント |#

3-6. 評価モデル

1. アトムは値そのもの(数値)またはシンボル束縛を返す
2. リスト (f a1 a2 ...) は:
   2-1. 各引数aiを評価
   2-2. 関数fを引数の値で呼ぶ
   2-3. 結果を返す
3. 特殊形式(if, let等)は独自の評価ルール
4. マクロは展開してから評価

3-7. このセクションのまとめ

- すべてはS式(アトムorリスト)
- 関数呼び出しは (関数 引数...)
- 前置記法、引数は任意個数
- 'exprで評価しないリテラル
- 「コードがデータ」の根拠

4. 基本データ型

4-1. 数値型

42                      ;; 整数(任意精度)
3.14                    ;; float
22/7                    ;; 有理数
#c(1 2)                 ;; 複素数1+2i
(expt 2 100)            ;; 1267650600228229401496703205376(巨大整数)

(integerp 42)           ;; T
(floatp 3.14)           ;; T
(rationalp 22/7)        ;; T
(numberp 42)            ;; T

Lispの整数は 任意精度。有理数も標準。


4-2. 文字列と文字

"hello"                 ;; 文字列
#\A                     ;; 文字(character)
#\Space                 ;; 空白文字
(length "hello")        ;; 5
(char "hello" 0)        ;; #\h
(subseq "hello" 1 3)    ;; "el"
(concatenate 'string "ab" "cd")  ;; "abcd"
(format nil "~a is ~a" "Alice" 30)  ;; "Alice is 30"

4-3. シンボル

'foo                    ;; シンボル
(symbolp 'foo)           ;; T
(symbol-name 'foo)       ;; "FOO"(デフォルトで大文字に)
(symbol-package 'foo)    ;; #<PACKAGE "COMMON-LISP-USER">

;; シンボルの値、関数、プロパティを別々に持てる
(setf (symbol-value 'x) 42)
(setf (symbol-function 'sq) (lambda (n) (* n n)))
(setf (get 'foo 'color) 'red)

シンボルは「名前 + 値 + 関数 + プロパティ」を持つ豊かなオブジェクト。


4-4. nilとt

nil                     ;; 偽 / 空リスト / 値なし、すべて同じ
t                       ;; 真
'()                     ;; nilと同じ
(eq nil '())             ;; T

(if nil 'yes 'no)        ;; NO
(if 'anything 'yes 'no)  ;; YES(nil以外はすべて真)

nil以外はすべて真」。Rubyと同じく、0や “” や '() ですらないnilだけが偽


4-5. リスト(consセル)

'(1 2 3)
(cons 1 '(2 3))         ;; (1 2 3)
(car '(1 2 3))           ;; 1
(cdr '(1 2 3))           ;; (2 3)
(cadr '(1 2 3))          ;; 2(car of cdr)
(list 1 2 3)             ;; (1 2 3)
(append '(1 2) '(3 4))   ;; (1 2 3 4)
(length '(a b c))        ;; 3
(reverse '(1 2 3))       ;; (3 2 1)

リストは consセルの連結。(cons a b) で「carがa、cdrがb」のセルを作る。

'(1 2 3) のメモリ表現:
[1 | *]→[2 | *]→[3 | nil]

4-6. 配列とベクタ

#(1 2 3)                ;; ベクタリテラル
(make-array 3 :initial-element 0)  ;; #(0 0 0)
(vector 1 2 3)           ;; #(1 2 3)
(aref #(10 20 30) 1)     ;; 20

リストよりランダムアクセスが速い。


4-7. ハッシュテーブル

(defvar h (make-hash-table))
(setf (gethash 'a h) 1)
(setf (gethash 'b h) 2)
(gethash 'a h)          ;; 1, T
(remhash 'a h)
(maphash (lambda (k v) (format t "~a=~a~%" k v)) h)

4-8. このセクションのまとめ

- 数値: 整数(任意精度)/ 有理数 / float / 複素数
- 文字列・文字(#\A)
- シンボル: 名前 + 値 + 関数 + プロパティ
- nil/t、nil以外すべて真
- リスト = consセル
- 配列・ハッシュテーブルも標準

5. 関数とラムダ

5-1. 関数定義

(defun greet (name)
  "Greet someone."     ;; docstring
  (format nil "Hello, ~a!" name))

(greet "World")        ;; "Hello, World!"

defun で関数定義。docstringが言語レベルで第一級


5-2. 引数の種類

;; オプション引数
(defun greet (name &optional (greeting "Hello"))
  (format nil "~a, ~a!" greeting name))

;; キーワード引数
(defun connect (host &key (port 80) (ssl nil))
  ...)
(connect "example.com" :port 443 :ssl t)

;; 可変長引数
(defun sum (&rest nums)
  (apply #'+ nums))
(sum 1 2 3 4)    ;; 10

;; すべて組み合わせ
(defun complex-fn (a b &optional c &rest rest &key (x 0)) ...)

5-3. ラムダ

((lambda (x) (* x x)) 5)    ;; 25
(funcall (lambda (x y) (+ x y)) 3 4)   ;; 7

;; 関数を変数に
(defvar f (lambda (x) (* x 2)))
(funcall f 10)          ;; 20

;; ラムダ短縮(リーダマクロ)
#'(lambda (x) (* x 2))   ;; 関数オブジェクトとして

#'function の糖衣。「シンボルではなく関数オブジェクトとして取る」。


5-4. 関数の名前空間

Lisp-2と呼ばれる 「変数と関数の名前空間が別」モデル。

(defvar list 10)        ;; 変数list
(list 1 2 3)            ;; 関数list(衝突しない)

(funcall list 1 2 3)    ;; 10を関数として呼ぼうとしてエラー
(funcall #'list 1 2 3)   ;; (1 2 3)

Scheme(Lisp-1)では同じ名前空間。Common Lispは意図的に分離。


5-5. 高階関数

(mapcar #'(lambda (x) (* x x)) '(1 2 3))   ;; (1 4 9)
(mapcar #'+ '(1 2 3) '(10 20 30))           ;; (11 22 33)
(reduce #'+ '(1 2 3 4))                      ;; 10
(remove-if #'evenp '(1 2 3 4 5))             ;; (1 3 5)
(find-if #'oddp '(2 4 5 6))                   ;; 5
(sort (copy-list '(3 1 4 1 5)) #'<)          ;; (1 1 3 4 5)

5-6. クロージャ

(defun make-counter ()
  (let ((count 0))
    (lambda ()
      (incf count))))

(defvar c (make-counter))
(funcall c)   ;; 1
(funcall c)   ;; 2
(funcall c)   ;; 3

外側の変数を捕捉。


5-7. このセクションのまとめ

- (defun name (args...) body)
- &optional / &key / &restで引数の種類
- (lambda (args) body) と #'(lambda ...)
- Lisp-2: 変数と関数の名前空間が別
- mapcar / reduce / remove-ifなどの高階関数
- クロージャ標準

6. 変数と束縛

6-1. グローバル変数

(defparameter *counter* 0)    ;; 慣習: グローバルは *...* で囲む
(defvar *config* nil)          ;; 既存の値があれば上書きしない

(setf *counter* 10)
*counter*                       ;; 10

defparameter は再評価で 再代入defvar は既存値があれば 保持


6-2. ローカル束縛:let / let*

(let ((x 10)
      (y 20))
  (+ x y))                      ;; 30

;; let* は順番に束縛(前の束縛が次で使える)
(let* ((x 10)
       (y (* x 2)))
  (+ x y))                      ;; 30

6-3. setfによる多目的代入

(setf x 10)                    ;; 変数
(setf (car my-list) 'new)      ;; リストのcar
(setf (gethash 'key h) 42)     ;; ハッシュ
(setf (aref arr 0) 100)        ;; 配列
(setf (slot-value obj 'name) "Alice")  ;; オブジェクトのスロット

setf は「任意の場所に代入」できる汎用代入。


6-4. 動的変数(special variable)

(defparameter *output* *standard-output*)

(defun print-msg (msg)
  (format *output* "~a~%" msg))

;; 一時的に出力先を変更
(let ((*output* (open "log.txt" :direction :output)))
  (print-msg "log message"))

defparameter / defvar で宣言された変数は 動的スコープlet で再束縛すると その動的範囲で新しい値になります。「慣習的に で囲む」のはこれが理由。


6-5. このセクションのまとめ

- defparameter / defvarでグローバル
- let / let* でローカル
- setfで汎用代入
- 動的変数(special variable)と動的スコープ
- グローバルは *name* の慣習

7. リスト処理

Lispの中核。リスト = データ構造かつコード

7-1. 基本操作

(car '(1 2 3))           ;; 1
(cdr '(1 2 3))           ;; (2 3)
(cons 1 '(2 3))          ;; (1 2 3)
(nth 2 '(a b c d))       ;; c(0始まり)
(last '(1 2 3))          ;; (3)
(length '(a b c))        ;; 3

(first '(a b c))         ;; a
(second '(a b c))        ;; b
(rest '(a b c))          ;; (b c)

first/restcar/cdr のエイリアス(読みやすい)。


7-2. リスト構築

(list 1 2 3)             ;; (1 2 3)
(append '(1 2) '(3 4))   ;; (1 2 3 4)
(reverse '(1 2 3))       ;; (3 2 1)
(remove 'b '(a b c b))   ;; (a c)
(remove-duplicates '(1 1 2 2 3))  ;; (1 2 3)

7-3. mapcarなど

(mapcar #'(lambda (x) (* x x)) '(1 2 3))   ;; (1 4 9)
(mapcar #'list '(1 2 3) '(a b c))           ;; ((1 a) (2 b) (3 c))
(maplist #'identity '(1 2 3))                ;; ((1 2 3) (2 3) (3))
(mapc #'print '(a b c))                       ;; 副作用、戻り値は元のリスト

7-4. 連想リスト(alist)

(defvar phone-book
  '((alice . "1234")
    (bob . "5678")
    (carol . "9012")))

(assoc 'bob phone-book)            ;; (bob . "5678")
(cdr (assoc 'bob phone-book))      ;; "5678"

軽量な辞書。小さいデータならハッシュより速い。


7-5. プロパティリスト(plist)

(defvar person '(:name "Alice" :age 30 :city "Tokyo"))

(getf person :age)        ;; 30
(setf (getf person :age) 31)

キーワード引数の内部表現にも使われる。


7-6. このセクションのまとめ

- car / cdr / cons / list / append / reverse
- first / second / ... / rest
- mapcar / reduce / remove-if
- alist(連想)とplist(プロパティ)も軽量辞書

8. 制御構造

8-1. if / when / unless

(if (> x 0) 'positive 'non-positive)

;; 単一分岐
(when (> x 0)
  (do-something)
  (do-another))

(unless (zerop x)
  (process x))

8-2. cond

(cond ((> x 0) 'positive)
      ((< x 0) 'negative)
      (t 'zero))

複数条件分岐の標準形。t はelseに相当。


8-3. case / typecase

(case status
  (200 "OK")
  ((404 410) "Not Found")
  (otherwise "Unknown"))

(typecase x
  (integer "int")
  (string "str")
  (list "list")
  (t "other"))

8-4. and / or

(and 1 2 3)      ;; 3(最後の真値)
(and 1 nil 3)    ;; nil(短絡評価)
(or nil nil 5)   ;; 5
(or 1 2 3)       ;; 1(最初の真値)

8-5. progn / prog1 / prog2

(progn
  (do-1)
  (do-2)
  (do-3))    ;; do-3の値を返す

(prog1 (do-1) (do-2) (do-3))   ;; do-1の値
(prog2 (do-1) (do-2) (do-3))   ;; do-2の値

8-6. このセクションのまとめ

- if / when / unless
- condで多分岐
- case / typecaseでパターンマッチ風
- and / orは最後の値を返す
- progn / prog1 / prog2で複数式の戻り値選択

9. シーケンスとイテレーション

9-1. dotimes / dolist

(dotimes (i 5)
  (print i))               ;; 0 1 2 3 4

(dolist (x '(a b c))
  (print x))                ;; a b c

9-2. loopマクロ(強力)

;; 基本
(loop for i from 1 to 10 collect i)         ;; (1 2 ... 10)
(loop for i below 10 collect i)              ;; (0 1 ... 9)
(loop for i from 0 by 2 below 10 collect i)  ;; (0 2 4 6 8)

;; リスト走査
(loop for x in '(a b c) collect (list x x))

;; 集約
(loop for x in '(1 2 3 4) sum x)              ;; 10
(loop for x in '(1 2 3 4) maximize x)         ;; 4
(loop for x in '(1 2 3 4) count (oddp x))     ;; 2

;; 条件
(loop for x in '(1 2 3 4)
      when (oddp x) collect x)                 ;; (1 3)

(loop for x in '(1 2 3 4)
      while (< x 3) collect x)                  ;; (1 2)

;; 並行イテレーション
(loop for k in '(a b c)
      for v in '(1 2 3)
      collect (cons k v))

loop「ミニ言語」と言えるほど強力。賛否両論あるが、書き始めると手放せない。


9-3. iterate / series(外部代替)

;; iterate(cl-iterate)
(iterate (for i from 1 to 10) (sum i))

loop より一貫性のある構文を求めるなら。


9-4. do / do*

;; 古典的、loopより低レベル
(do ((i 0 (+ i 1))
     (sum 0 (+ sum i)))
    ((= i 10) sum))     ;; 終了条件と戻り値

9-5. このセクションのまとめ

- dotimes / dolistで単純ループ
- loopマクロが強力(独自のDSL)
- do / do* が低レベル
- iterate(外部)が一貫性で人気
- mapcarなど関数型スタイルも併用

10. マクロ

Lispの 真髄。「コードを生成するコード」。

10-1. なぜマクロか

;; ifが無い言語でORを作りたいとしたら...

;; 関数だと両方評価されてしまう
(defun my-or (a b)
  (if a a b))

(my-or t (do-something-expensive))   ;; do-something-expensiveが呼ばれる!

;; マクロなら遅延評価
(defmacro my-or (a b)
  `(if ,a ,a ,b))

(my-or t (do-something-expensive))   ;; do-something-expensiveは評価されない

マクロコンパイル時に展開される。引数を評価する前に処理できる。


10-2. quasiquoteとunquote

`(1 2 3)                ;; '(1 2 3) と同じ
`(1 ,(+ 2 3))            ;; (1 5) :, で評価
`(1 ,@(list 2 3))        ;; (1 2 3) :,@ で展開
  • ` (backquote): リストテンプレート
  • ,: 評価して埋め込む
  • ,@: 評価してリストを展開

10-3. defmacro

(defmacro when-positive (x &body body)
  `(if (> ,x 0)
       (progn ,@body)))

(when-positive 5
  (print "positive!")
  (print "really!"))

;; 展開:
;; (if (> 5 0)
;;     (progn (print "positive!") (print "really!")))

&body は本質的に &rest と同じだが、「コードブロック」を意図することを表す。


10-4. macroexpand

マクロが展開された結果を確認:

(macroexpand '(when-positive x (print x)))
;; (if (> x 0) (progn (print x))) を返す

(macroexpand-1 ...)    ;; 1段階だけ

10-5. 衛生(hygiene)とgensym

;; Bad: 変数衝突の危険
(defmacro my-let ((var val) &body body)
  `(let ((,var ,val))
     ,@body))

(my-let (it 10)
  (print it))    ;; OK

;; しかし...
(defmacro swap (a b)
  `(let ((tmp ,a))
     (setf ,a ,b)
     (setf ,b tmp)))

(let ((tmp 100) (other 200))
  (swap tmp other))    ;; tmpの名前衝突!

gensymで解決

(defmacro swap (a b)
  (let ((tmp-sym (gensym)))
    `(let ((,tmp-sym ,a))
       (setf ,a ,b)
       (setf ,b ,tmp-sym))))

gensym一意のシンボルを生成。Common Lispは 非衛生マクロなので、自分で対処する必要があります。Schemeは衛生マクロ(自動)。


10-6. マクロの実例

;; with-* パターン
(defmacro with-open-file ((stream filename &rest args) &body body)
  `(let ((,stream (open ,filename ,@args)))
     (unwind-protect
         (progn ,@body)
       (close ,stream))))

(with-open-file (s "data.txt")
  (read s))

with-open-file は実は標準のマクロ。Lispの 「リソース管理パターン」の典型。


10-7. このセクションのまとめ

- マクロは「コードを生成するコード」
- defmacro / `(quasiquote)/ , (unquote)/ ,@ (splice)
- macroexpandで確認
- gensymで衛生確保(非衛生マクロ)
- with-* パターンでRAII風
- 言語自体を拡張可能

11. CLOS(Common Lisp Object System)

LispのOOPシステム。多重継承・多重ディスパッチ・メタオブジェクトプロトコルを持つ最強クラスのOOP。

11-1. クラス定義

(defclass animal ()
  ((name :initarg :name :accessor name)
   (age :initarg :age :accessor age :initform 0)))

(defclass dog (animal)
  ((breed :initarg :breed :accessor breed)))

スロット指定

:initarg :name      ;; make-instanceの引数キー
:accessor name      ;; getter/setter(name, (setf name))
:reader name         ;; getterのみ
:writer (setf name)  ;; setterのみ
:initform 0          ;; デフォルト値
:type integer         ;; 型注釈
:allocation :class   ;; クラススロット(インスタンス共有)

11-2. インスタンス生成

(defvar d (make-instance 'dog :name "Rex" :age 5 :breed "Lab"))
(name d)    ;; "Rex"
(age d)     ;; 5
(setf (name d) "Buddy")

11-3. メソッド(generic function)

(defgeneric speak (animal)
  (:documentation "Make a sound"))

(defmethod speak ((a animal))
  "...")

(defmethod speak ((d dog))
  "Woof!")

(speak (make-instance 'dog))    ;; "Woof!"

defgeneric で総称関数、defmethod で型ごとの実装。


11-4. 多重ディスパッチ

(defmethod combat ((attacker dog) (target cat))
  "Dog chases cat")

(defmethod combat ((attacker dog) (target dog))
  "Dogs play")

(defmethod combat ((attacker cat) (target mouse))
  "Cat hunts mouse")

複数引数の型でメソッド選択。これがJava/C# の単一ディスパッチとの大きな違い。


11-5. メソッドコンビネーション

(defmethod speak :before ((a animal))
  (format t "Animal speaks: "))

(defmethod speak ((d dog))
  "Woof!")

(speak (make-instance 'dog))
;; 出力: Animal speaks: 
;; 戻り値: "Woof!"

:before:after:aroundメソッド呼び出しをラップ。AOP的なことが可能。


11-6. 多重継承

(defclass swimmer () ((swim-speed :initarg :swim-speed)))
(defclass flyer () ((fly-speed :initarg :fly-speed)))
(defclass duck (animal swimmer flyer) ())

C++ の多重継承相当だが、CLOSは C3線形化でMROを解決。


11-7. このセクションのまとめ

- defclass / defmethod / defgeneric
- 多重継承可能
- 多重ディスパッチ(複数引数の型で選択)
- :before / :after / :aroundコンビネーション
- メタオブジェクトプロトコル(19章)

12. パッケージとシンボル

12-1. パッケージ

(defpackage :my-app
  (:use :cl)               ;; common-lispパッケージを継承
  (:export :main :run))

(in-package :my-app)

シンボルの名前空間」。Javaのパッケージ、Pythonのモジュール相当。


12-2. シンボルの参照

my-app:main          ;; exportされたシンボル
my-app::internal     ;; exportされていないシンボル(::)

: はpublic、:: はprivate含む。


12-3. import / shadowing

(defpackage :my-app
  (:use :cl :alexandria)              ;; 全exportを取り込み
  (:import-from :hunchentoot
                #:start-server
                #:stop-server)
  (:shadowing-import-from :other #:reduce))

12-4. このセクションのまとめ

- defpackageで名前空間定義
- :useで他パッケージを継承
- :exportで公開シンボル
- :import-fromで個別import
- pkg:name(public)/ pkg::name(private)

13. コンディションシステム(例外処理)

Common Lispの例外処理は 「他言語より一段強力」。「復帰可能な条件」を扱える。

13-1. 基本的な例外

(handler-case
    (progn
      (when bad
        (error "Something went wrong: ~a" reason))
      (do-stuff))
  (simple-error (c)
    (format t "Caught: ~a~%" c))
  (error (c)
    (format t "Generic: ~a~%" c)))

13-2. 独自コンディション

(define-condition my-error (error)
  ((code :initarg :code :reader my-error-code)
   (msg :initarg :msg :reader my-error-msg))
  (:report (lambda (c stream)
             (format stream "Error ~a: ~a"
                     (my-error-code c) (my-error-msg c)))))

(error 'my-error :code 42 :msg "oops")

13-3. restart(復帰可能)

(defun read-number (stream)
  (let ((line (read-line stream)))
    (handler-bind
        ((parse-error
          (lambda (c)
            (declare (ignore c))
            (invoke-restart 'use-default 0))))
      (restart-case
          (parse-integer line)
        (use-default (default)
          :report "Use default value"
          default)))))

エラーが起きた箇所で、別の戦略を選ぶ」ことが可能。Javaの例外より強力で、これが デバッガで対話的に復帰できる仕組み。


13-4. handler-bind vs handler-case

handler-case:  Javaのtry-catch相当(スタックを巻き戻す)
handler-bind:  ハンドラ実行後、復帰可能(スタックを保持)

13-5. このセクションのまとめ

- handler-caseで例外捕捉
- error / signalでコンディション発生
- restart-caseで復帰選択肢を定義
- handler-bindは復帰可能
- 「エラー箇所で対話的に修復」

14. 配列・文字列・ハッシュテーブル

14-1. 配列

(make-array 5)                    ;; #(0 0 0 0 0)
(make-array 5 :initial-element 'x)
(make-array '(2 3))                ;; 多次元
(aref arr 0)
(setf (aref arr 0) 'foo)

;; ベクタ
(vector 1 2 3)                     ;; #(1 2 3)
#(1 2 3)                            ;; リテラル

;; 動的配列
(make-array 0 :fill-pointer 0 :adjustable t)
(vector-push-extend x v)

14-2. 文字列

"hello"
(length "hello")                  ;; 5
(char "hello" 0)                  ;; #\h
(subseq "hello" 1 4)              ;; "ell"
(concatenate 'string "ab" "cd")   ;; "abcd"
(string-upcase "hello")            ;; "HELLO"
(format nil "~a is ~a" "Alice" 30)

文字列は 文字の配列


14-3. ハッシュテーブル

(defvar h (make-hash-table :test 'equal))
(setf (gethash "alice" h) 1)
(gethash "alice" h)                ;; 1, T
(remhash "alice" h)
(hash-table-count h)
(maphash (lambda (k v) (format t "~a=~a~%" k v)) h)

;; 各種テスト関数
:test 'eq                           ;; ポインタ
:test 'eql                          ;; 数値・文字含む
:test 'equal                        ;; 構造的(リスト・文字列)
:test 'equalp                       ;; さらに緩い(大小無視等)

14-4. このセクションのまとめ

- make-arrayで多次元配列
- vector / vector-push-extend
- 文字列は文字の配列
- ハッシュはmake-hash-table、:testを選ぶ
- maphashでイテレーション

15. ファイルI/Oとストリーム

;; 読み込み
(with-open-file (s "data.txt" :direction :input)
  (loop for line = (read-line s nil)
        while line
        do (format t "~a~%" line)))

;; 書き込み
(with-open-file (s "out.txt"
                  :direction :output
                  :if-exists :supersede
                  :if-does-not-exist :create)
  (format s "hello~%"))

;; S式の入出力
(with-open-file (s "data.lisp" :direction :output :if-exists :supersede)
  (print '(1 2 3) s)
  (print '(a b c) s))

(with-open-file (s "data.lisp" :direction :input)
  (let ((data (read s)))
    (print data)))

with-open-file自動クローズ。Pythonの with 相当。


16. 並行処理とスレッド

Common Lisp標準にはスレッドがないが、各処理系が拡張を提供。bordeaux-threads が事実上の標準ポータブルライブラリ。

(ql:quickload :bordeaux-threads)

(bt:make-thread
 (lambda ()
   (sleep 1)
   (print "from thread")))

(defvar *lock* (bt:make-lock))
(bt:with-lock-held (*lock*)
  ...)

SBCL強力なスレッドサポート、各種Lisp Machineの伝統で並行プリミティブも豊富。

Common Lispで並行処理を書くときは、まず「処理系依存の機能を直接使うか、移植性のために抽象化するか」を決める。SBCLだけを対象にするなら sb-thread を直接使えるが、ライブラリとして公開するなら bordeaux-threads を挟むほうが安全である。

実務では、スレッドを増やす前に共有状態を減らす。動的変数、グローバルなハッシュテーブルキャッシュ、ロギング先を複数スレッドから触ると、REPLでは再現しにくい不具合になりやすい。ミューテックスで守るだけでなく、キューで仕事を渡す、ワーカーごとに状態を分離する、純粋な計算部分を関数として切り出す、という順で設計すると読みやすい。

また、Common Lispの強みである動的開発は並行処理と相性が難しい。実行中に関数を再定義できることは便利だが、ワーカースレッドが古い定義と新しい定義をまたいで動くことがある。長く動くプロセスでは、再定義の単位を小さくし、状態移行を明示し、テスト用の小さなスレッド構成で先に確認するのが現実的である。


17. 動的開発(REPL駆動)

Lispの 真髄。「実行中のプログラムを書き換える」開発スタイル。

17-1. 実行中の再定義

;; 関数を定義
(defun greet (name) (format t "Hi, ~a~%" name))

;; 実行
(greet "Alice")    ;; Hi, Alice

;; 関数を再定義(プログラムを止めずに)
(defun greet (name) (format t "Hello, ~a!~%" name))

;; もう一度実行
(greet "Alice")    ;; Hello, Alice!

REPLで実行しながら、エディタから関数定義を送信して 実行中のプロセスに反映。これがLisp開発の核心。


17-2. デバッガでの復帰

(defun divide (x y) (/ x y))
(divide 10 0)
;; → コンディション、デバッガが起動
;; リスタート選択肢:
;;   0: [retry] 再試行
;;   1: [return] 別の値を返す
;;   2: [abort] 中止

;; この場でyを1にして再計算、など対話的に修復可能

エラーで止まったその場で修復してプログラムを継続」できる。SBCL + SLIMEで体験できる。


17-3. このセクションのまとめ

- REPLでコードを書きながら実行
- 関数を再定義しても実行中プロセスに反映
- デバッガで復帰可能
- 「Image-based」開発スタイル
- 他言語にはない体験

18. リーダマクロ

ソースコードのレキサ/パーサ自身をカスタマイズ。最強のメタプログラミング

;; #' は実は (function ...) のリーダマクロ
;; ` は (quasiquote ...)
;; ' は (quote ...)
;; #| ... |# はブロックコメント

;; 自作リーダマクロ
(set-dispatch-macro-character #\# #\$
  (lambda (stream char1 char2)
    (declare (ignore char1 char2))
    `(format nil "$~a" ,(read stream))))

#$"100"     ;; "$100"

言語の構文自体を拡張」できる。例: JSON-like構文、SQL DSL、HTMLテンプレート。


19. メタオブジェクトプロトコル(MOP)

CLOS自体を メタオブジェクトとして操作。クラス定義の挙動・メソッドディスパッチを ユーザがカスタマイズ可能

(use-package :closer-mop)

;; クラスのスロットを動的に取得
(class-slots (find-class 'my-class))

;; カスタムメタクラス
(defclass tracking-class (standard-class) ...)

(defclass tracked-object ()
  ((value))
  (:metaclass tracking-class))

OOPシステム自体をカスタマイズ」できる稀有な言語機能。SmalltalkとCommon Lispだけ。


20. 性能とコンパイル

20-1. コンパイル

Common Lispは 動的言語だが事前コンパイル可能SBCLのネイティブコンパイル性能は Cに肉薄することがある。

(defun fib (n)
  (declare (type fixnum n)
           (optimize (speed 3) (safety 0)))
  (if (< n 2) n
      (+ (fib (- n 1)) (fib (- n 2)))))

(compile 'fib)
(time (fib 30))

declare型・最適化指示(optimize (speed 3) (safety 0)) でCレベルの最適化。


20-2. ベンチマーク

(time (loop for i from 1 to 1000000 sum i))
;; Evaluation took:
;;   0.001 seconds of real time
;;   0.001000 seconds of total run time (0.001000 user, 0.000000 system)
;;   ...

20-3. このセクションのまとめ

- compileで関数を機械語化
- declareで型・最適化指示
- (optimize (speed 3)) で最大速度
- SBCLはCに肉薄
- timeでベンチマーク

21. ASDFとプロジェクト構成

ASDFはCommon Lispの ビルドシステム。JavaのMaven相当。

my-project/
├── my-project.asd      # システム定義
├── package.lisp
├── main.lisp
└── tests/
    └── test.lisp
;; my-project.asd
(asdf:defsystem #:my-project
  :description "..."
  :author "Me"
  :license "MIT"
  :version "0.0.1"
  :depends-on (#:alexandria #:cl-ppcre)
  :components ((:file "package")
               (:file "main" :depends-on ("package"))))
;; ロード
(asdf:load-system :my-project)
;; または
(ql:quickload :my-project)

22. テスト戦略

主要テストフレームワーク:

FiveAM:    最も人気
prove:     簡潔
parachute: モダン
rove:      新しめ
(ql:quickload :fiveam)

(fiveam:test addition
  (fiveam:is (= 3 (+ 1 2)))
  (fiveam:is (= 0 (+ -1 1))))

(fiveam:run! 'addition)

23. 実装の選び方

SBCL速度・標準・人気。新規学習推奨
CCL     Macで速い、起動が軽い
ECL     C統合、組み込み
ABCL    JVM連携が必要なら
CLISPバイトコード、移植性重視
LispWorks商用、IDE
Allegro CL商用、エンタープライズ

学習用と実務用で最初の選び方は変わる。言語そのものを学ぶなら、ドキュメントと利用者が多く、Quicklispとの組み合わせが扱いやすいSBCLから始めるのが自然である。実行速度、ネイティブコード生成、開発環境との相性もよく、Common Lispの標準的な感覚をつかみやすい。

一方で、Cライブラリとの密な連携や組み込み用途ではECL、JVM上の既存資産と接続したい場合はABCLが候補になる。商用サポート、GUI、長期保守、専用IDEを重視する現場ではLispWorksやAllegro CLも検討対象になる。処理系の違いは単なる速度差ではなく、デプロイ方法、FFI、スレッド、デバッガ、ライセンス、サポート体制まで含めて判断する。

迷ったときは、次の順に確認するとよい。

  • ローカルでQuicklispが素直に動くか。
  • 利用したいライブラリが対象処理系でテストされているか。
  • 本番環境への配置方法が明確か。
  • デバッガとプロファイラを日常的に使えるか。
  • チーム内で再現可能な開発手順を文書化できるか。

24. 他のLispとの比較(Scheme / Clojure / Emacs Lisp)

Common Lisp:
  + 大規模、多パラダイム、CLOS
  + ANSI標準、長期安定
  + マクロ強力(非衛生)
  - 構文が独特、初心者向きでない

Scheme:
  + ミニマリスト、教育向け
  + 衛生マクロ(hygienic)
  - 標準ライブラリが小さい

Clojure:
  + JVM上、Javaエコシステム
  + イミュータブル中心、関数型
  + 並行処理プリミティブ豊富
  - JVMの起動が遅い

Emacs Lisp:
  + Emacs拡張専用
  - 言語仕様としては素朴

25. 主要ライブラリ

alexandria:        標準ライブラリ拡張
cl-ppcre:           正規表現
hunchentoot:       Webサーバ
cl-who:             HTMLテンプレート
parenscript:       JS生成
clack / lack:      Webフレームワーク
caveman:           Webフレームワーク(Sinatra風)
postmodern:        PostgreSQLクライアント
mito:              ORM
bordeaux-threads:  スレッド
cl-async:          非同期I/O
optima / trivia:   パターンマッチング
fiveam:            テスト
asdf:              ビルド
quicklisp:         パッケージマネージャ
closer-mop:        MOP

26. よくある落とし穴FAQ

Q1. デフォルトで大文字になる

シンボルは内部で大文字化される。(defun Foo ...) でも内部では FOO|Foo| でケース保持可能。

Q2. NILは何でもない

nil = '() = false。空リストと偽値を兼ねる。

Q3. funcallapply の違い

(funcall #'+ 1 2 3)        ;; 6
(apply #'+ '(1 2 3))        ;; 6(最後の引数がリスト)
(apply #'+ 1 2 '(3 4))      ;; 10

Q4. setfsetq の違い

setq は変数のみ。setf任意の場所(リスト要素、ハッシュエントリ等)。

Q5. eq / eql / equal / equalp

eq:    ポインタ等価(最速)
eql:   eq + 数値・文字
equal: 構造的(リスト・文字列)
equalp: さらに緩い(大小無視、配列内容)

Q6. defstructとdefclass

defstruct は軽量・速い、defclass はCLOSの強力さ。新規はCLOS。

Q7. 並行性

標準にはない、bordeaux-threadsがポータブル。SBCL強力

Q8. パッケージ汚染

(use-package ...) で全exportを継承するとシンボル衝突。:import-from で個別取り込みが安全。

Q9. 動的変数(special)の宣言忘れ

(defvar) していない変数を let で再束縛するとローカル化される(注意)。(declaim (special *foo*)) で明示。

Q10. ループはloopで十分?

loop強力だが構文が独特。iterateseriesdo、再帰、mapcar を場面で使い分け。


27. 実践パターン集

27-1. with-* マクロ

(defmacro with-resource ((var resource) &body body)
  `(let ((,var ,resource))
     (unwind-protect
         (progn ,@body)
       (cleanup ,var))))

リソース管理の定型。


27-2. ジェネリック関数の継承

(defmethod process :around ((obj t))
  (log-entry obj)
  (let ((result (call-next-method)))
    (log-exit result)
    result))

27-3. DSL構築

(defmacro html-page (&body body)
  `(progn
     (princ "<!DOCTYPE html><html>")
     ,@body
     (princ "</html>")))

(html-page
  (princ "<body>Hello</body>"))

Lispで言語を作る」のがLispの楽しさ。


28. 学習ロードマップ(30日)

Week 1: 基礎

  • SBCL + Quicklisp + Emacs/SLIME
  • S式・関数・変数
  • リスト処理
  • 制御構造

Week 2: 中級

  • マクロ入門
  • CLOS入門
  • パッケージ
  • コンディション

Week 3: 実践

  • ASDFプロジェクト構築
  • ライブラリ活用(alexandria, cl-ppcre)
  • ファイルI/O
  • スレッド

Week 4: 上級

  • 自作マクロでDSL
  • CLOSでOOPプロジェクト
  • 自作ライブラリをQuicklisp風に整備
  • “Practical Common Lisp” を読了

29. 用語集

あ行

  • アトム: リストでないS式(数値、シンボル、文字列)

か行

  • 括弧(パーレン): Lispの特徴
  • クォート('): 評価しない
  • クロージャ: 環境を捕獲する関数

さ行

  • シンボル: 名前 + 値 + 関数 + プロパティ
  • スロット: CLOSのフィールド
  • スペシャル変数: 動的スコープ変数

た行

  • 多重ディスパッチ: 複数引数の型でメソッド選択
  • 動的型付け: 実行時型決定

な行

は行

  • パッケージ: 名前空間
  • バックティック(`): quasiquote
  • 不変: 純粋関数型機能(Common Lispは可変もOK)

ま行

  • マクロ: コードを生成するコード

A〜Z

  • ANSI: ANSI X3.226-1994標準
  • ASDF: Another System Definition Facility
  • CLOS: Common Lisp Object System
  • CL: Common Lisp
  • MOP: Metaobject Protocol
  • REPL: Read-Eval-Print Loop
  • S式: Symbolic Expression
  • SBCL: Steel Bank Common Lisp
  • SLIME: Superior Lisp Interaction Mode for Emacs

発展: 言語機能

ここからはCommon Lispの各機能を 実例とともに深掘りマクロ、CLOS、コンディションシステム、Web開発、性能、メタプログラミングまで詳細に。


31. マクロ完全活用

31-1. マクロを書く順序

1. まず関数として書けるか考える
2. 評価順序を制御したい / コードを生成したいならマクロ
3. macroexpandで展開を確認
4. gensymで衛生確保
5. テストを書く

31-2. with-* パターン

(defmacro with-resource ((var resource-form) &body body)
  (let ((resource-sym (gensym)))
    `(let* ((,resource-sym ,resource-form)
            (,var ,resource-sym))
       (unwind-protect
            (progn ,@body)
         (cleanup ,resource-sym)))))

(with-resource (db (open-database "..."))
  (query db "SELECT * FROM users"))

with-open-filewith-output-to-string など標準ライブラリの定型。


31-3. アナフォリックマクロ

(defmacro aif (test then &optional else)
  `(let ((it ,test))
     (if it ,then ,else)))

(aif (find-user 1)
     (format t "Found ~a" it)        ;; itに評価結果が束縛
     (format t "Not found"))

it という暗黙の変数を導入。Lispの柔軟性を象徴。


31-4. DSL: HTML生成

(defmacro html (&body body)
  `(with-output-to-string (s)
     ,@(mapcar #'compile-html body)))

(defun compile-html (form)
  (cond
    ((stringp form) `(princ ,form s))
    ((listp form)
     (let ((tag (car form))
           (children (cdr form)))
       `(progn
          (format s "<~a>" ',tag)
          ,@(mapcar #'compile-html children)
          (format s "</~a>" ',tag))))))

(html
  (html
    (head (title "My Page"))
    (body (h1 "Hello") (p "World"))))

LispでHTMLテンプレートを書く」DSL。cl-who などのライブラリが本格実装。


31-5. このセクションのまとめ

- with-* パターンでRAII風
- アナフォリック(itを導入)
- DSL構築
- gensymで衛生
- macroexpandで確認

32. CLOS完全活用

32-1. クラスとスロット

(defclass vehicle ()
  ((wheels :initarg :wheels :accessor wheels)
   (color :initarg :color :accessor color :initform 'white)))

(defclass car (vehicle)
  ((seats :initarg :seats :accessor seats :initform 4)))

(defparameter c (make-instance 'car :wheels 4 :color 'red :seats 5))
(wheels c)        ;; 4
(color c)         ;; RED
(seats c)         ;; 5

32-2. 多重ディスパッチの威力

(defgeneric collide (a b))

(defmethod collide ((a asteroid) (b spaceship))
  (destroy b))

(defmethod collide ((a spaceship) (b asteroid))
  (destroy a))

(defmethod collide ((a spaceship) (b spaceship))
  (destroy a)
  (destroy b))

(defmethod collide ((a asteroid) (b asteroid))
  (split a)
  (split b))

両方の引数の型でメソッド選択」。Java/Pythonでは難しいパターン。


32-3. メソッドコンビネーション詳細

(defmethod handle-request :before ((req request))
  (log-request req))

(defmethod handle-request ((req request))
  (process req))

(defmethod handle-request :after ((req request))
  (log-response (response req)))

(defmethod handle-request :around ((req request))
  (with-timing
    (call-next-method)))

実行順序:

1. :aroundの最外側
2. :beforeすべて
3. プライマリ(:around内のcall-next-method)
4. :afterすべて(逆順)
5. :aroundの最外側終了

AOP(Aspect-Oriented Programming)相当を 言語標準で。


32-4. メタクラス(MOP)

(defclass tracking-class (standard-class) ())

(defmethod validate-superclass ((class tracking-class) (super standard-class))
  t)

(defmethod make-instance :before ((class tracking-class) &rest args)
  (declare (ignore args))
  (incf (slot-value class 'instance-count)))

OOPシステム自体をカスタマイズ」。MOP(Metaobject Protocol)の威力。


32-5. このセクションのまとめ

- defclass / defmethod / defgeneric
- 多重ディスパッチ(複数引数の型)
- :before / :after / :aroundコンビネーション
- メタクラスでOOP自体を拡張

33. コンディションシステム詳細

33-1. シンプルな例

(handler-case
    (parse-integer "abc")
  (parse-error (c)
    (format t "Parse failed: ~a~%" c)
    0))

33-2. restartの真の威力

(define-condition not-found-error (error)
  ((key :initarg :key :reader nf-key))
  (:report (lambda (c stream)
             (format stream "Not found: ~a" (nf-key c)))))

(defun lookup-with-default (key default)
  (handler-bind
      ((not-found-error
        (lambda (c)
          (declare (ignore c))
          (invoke-restart 'use-default))))
    (restart-case
        (do-lookup key)
      (use-default ()
        :report "Use default value"
        default))))

エラー発生地点で復帰戦略を選ぶ」。Java/Pythonの例外より遥かに強力


33-3. デバッガでの対話的復帰

(defun divide (x y)
  (if (zerop y)
      (restart-case (error "Division by zero")
        (use-value (val)
          :report "Use a value instead"
          :interactive (lambda () (list (read)))
          val)
        (use-zero ()
          :report "Treat as zero"
          0))
      (/ x y)))

(divide 10 0)
;; → デバッガ起動、リスタート選択肢を表示
;; ユーザが対話的に修復

プログラムを止めずに、その場で修復」できる仕組み。


33-4. このセクションのまとめ

- handler-case(スタック巻き戻し)
- handler-bind(復帰可能)
- restart-caseでリスタート定義
- invoke-restartで選択
- デバッガで対話的復帰
- 他言語にない強力な仕組み

34. パッケージとシンボル詳細

34-1. パッケージの完全な例

(defpackage :myapp.core
  (:use :cl)
  (:nicknames :app)
  (:export #:start
           #:stop
           #:*config*
           #:make-instance)
  (:shadow #:open #:close)        ;; clの同名と衝突回避
  (:import-from :alexandria
                #:flatten
                #:hash-table-keys)
  (:documentation "My App Core"))

(in-package :myapp.core)

34-2. シンボルアクセス

:keyword           ;; キーワードシンボル(KEYWORDパッケージ)
'cl:car            ;; CLパッケージのcar
'myapp:start        ;; 公開シンボル
'myapp::internal    ;; 非公開シンボル(::)
'#:uninterned      ;; uninternedシンボル

34-3. パッケージシステムの注意

- (use-package ...) はシンボル衝突に注意
- :import-fromで個別取り込みが安全
- :shadowing-import-fromで意図的に上書き

34-4. このセクションのまとめ

- defpackageで名前空間定義
- :use, :export, :import-from, :shadow
- パッケージ修飾: pkg:name / pkg::name
- :keywordは特殊

35. ASDFとプロジェクト管理

35-1. プロジェクト構造

my-project/
├── my-project.asd
├── package.lisp
├── src/
│   ├── core.lisp
│   ├── utils.lisp
│   └── main.lisp
├── tests/
│   ├── test-package.lisp
│   └── core-tests.lisp
├── README.md
└── LICENSE

35-2. ASDFシステム定義

;; my-project.asd
(asdf:defsystem #:my-project
  :description "My awesome project"
  :author "Me <me@example.com>"
  :license "MIT"
  :version "0.1.0"
  :depends-on (#:alexandria
               #:cl-ppcre
               #:bordeaux-threads)
  :components ((:file "package")
               (:module "src"
                :components
                ((:file "utils" :depends-on ("../package"))
                 (:file "core" :depends-on ("utils"))
                 (:file "main" :depends-on ("core")))))
  :in-order-to ((test-op (test-op #:my-project/tests))))

(asdf:defsystem #:my-project/tests
  :depends-on (#:my-project #:fiveam)
  :components ((:file "tests/test-package")
               (:file "tests/core-tests"))
  :perform (test-op (op c)
             (uiop:symbol-call :fiveam :run! 'my-project.tests:all-tests)))

35-3. システムのロード

(asdf:load-system :my-project)
;; または
(ql:quickload :my-project)

35-4. このセクションのまとめ

- ASDFが標準ビルドツール
- system定義で依存・モジュール
- Quicklisp経由が一般的
- test-opでテスト統合

36. Web開発(Hunchentoot / Clack)

36-1. Hunchentoot

(ql:quickload :hunchentoot)

(defparameter *server*
  (make-instance 'hunchentoot:easy-acceptor :port 8080))

(hunchentoot:define-easy-handler (greet :uri "/greet") (name)
  (format nil "Hello, ~a!" (or name "World")))

(hunchentoot:start *server*)

LispのTomcat」と呼ばれるWebサーバ。


36-2. Clack(Webフレームワーク)

(ql:quickload :clack)
(ql:quickload :ningle)

(defvar *app* (make-instance 'ningle:app))

(setf (ningle:route *app* "/")
      (lambda (params)
        "Hello, World!"))

(setf (ningle:route *app* "/users/:id")
      (lambda (params)
        (format nil "User ~a" (cdr (assoc :id params)))))

(clack:clackup *app* :port 8080)

Sinatra / Express風の軽量フレームワーク。


36-3. このセクションのまとめ

- Hunchentoot: 重量級Webサーバ
- Clack: WSGI / Rack相当
- Ningle: Sinatra風
- Caveman: より高機能

37. データベース連携

37-1. cl-postgres / postmodern

(ql:quickload :postmodern)

(postmodern:with-connection ("mydb" "user" "pass" "localhost")
  (postmodern:query "SELECT * FROM users WHERE id = $1" 1)
  ;; 結果はリストのリスト
)

;; ORM
(defclass user ()
  ((id :col-type integer :col-identity t)
   (name :col-type string :initarg :name)
   (email :col-type string :initarg :email))
  (:metaclass postmodern:dao-class)
  (:keys id))

(postmodern:make-dao 'user :name "Alice" :email "a@b.c")

37-2. mito(ORM)

(mito:deftable user ()
  ((name :col-type :text)
   (email :col-type :text)))

(mito:create-dao 'user :name "Alice" :email "a@b.c")
(mito:select-dao 'user (sxql:where (:= :name "Alice")))

37-3. このセクションのまとめ

- postmodern: PostgreSQLクライアント + DAO
- mito: ActiveRecord風ORM
- cl-dbi: 抽象化レイヤー
- sxql: SQL DSL

38. 並行・並列処理

38-1. bordeaux-threads

(ql:quickload :bordeaux-threads)

(defvar *threads* nil)

(dotimes (i 4)
  (push
    (bt:make-thread
     (lambda ()
       (format t "Thread ~a~%" i)
       (sleep 1))
     :name (format nil "worker-~a" i))
    *threads*))

(dolist (th *threads*)
  (bt:join-thread th))

38-2. ロック

(defvar *lock* (bt:make-lock))
(defvar *counter* 0)

(bt:with-lock-held (*lock*)
  (incf *counter*))

38-3. lparallel(データ並列)

(ql:quickload :lparallel)
(setf lparallel:*kernel* (lparallel:make-kernel 4))

(lparallel:pmap 'list (lambda (x) (* x x)) '(1 2 3 4 5))
;; → (1 4 9 16 25)、4並列実行

38-4. このセクションのまとめ

- bordeaux-threads: ポータブルスレッド
- ロック / mutex
- lparallel: データ並列
- SBCLは強力なネイティブスレッド

39. 性能とコンパイル詳細

39-1. 型宣言で高速化

(defun fast-sum (arr)
  (declare (type (simple-array (signed-byte 64) (*)) arr)
           (optimize (speed 3) (safety 0) (debug 0)))
  (let ((sum 0))
    (declare (type (signed-byte 64) sum))
    (loop for i from 0 below (length arr)
          do (incf sum (aref arr i)))
    sum))

(compile 'fast-sum)

型 + 最適化指示」で Cに肉薄する性能。


39-2. SBCLのディスアセンブル

(disassemble #'fast-sum)
;; アセンブリ出力で最適化を確認できる

39-3. このセクションのまとめ

- (declare (type ...)) で型宣言
- (optimize (speed 3) (safety 0)) で最適化
- compileで機械語化
- disassembleで結果確認
- SBCLのネイティブコンパイラは強力

40. 主要ライブラリ深掘り

alexandria:    標準ライブラリ拡張(必携)
cl-ppcre:       正規表現
serapeum:       より大きなutility
trivia / optima: パターンマッチング
fiveam / parachute / rove: テスト
hunchentoot / clack: Web
caveman: フルスタックWeb
ningle: Sinatra風
postmodern: PostgreSQL
mito / cl-dbi: ORM / DB抽象
bordeaux-threads: スレッド
lparallel:      並列
log4cl: ロギング
yason / cl-json: JSON
cl-async: 非同期I/O
cl-cron: スケジュール
clingon:        CLI

41. Common Lisp拡張FAQ

Q1. なぜ大文字になる?

'Foo
;; → FOO

リーダがデフォルトで大文字化する。|Foo| で大小保持可能。

Q2. Schemeとの違い

Common Lisp:
  - Lisp-2(変数vs関数の名前空間別)
  - 大規模、機能豊富
  - 非衛生マクロ
  - ANSI標準

Scheme:
  - Lisp-1(同じ名前空間)
  - ミニマリスト
  - 衛生マクロ(hygienic)
  - R5RS / R6RS / R7RS標準

Q3. NILと '() は同じ?

(eq nil '())    ;; T
;; nil = '() = false全部同じ

Q4. defunとdefmethod

defun:    通常関数(単一実装)
defmethod: 総称関数のメソッド(多重ディスパッチ)

Q5. setqとsetf

setq は変数のみ。setf任意の場所(リスト要素、ハッシュ、structスロットなど)。setf を使う。

Q6. eq / eql / equal / equalpの使い分け

eq:    ポインタ等価(最速、シンボル比較に)
eql:   eq + 数値・文字(数値比較に)
equal: 構造的(リスト・文字列、ほとんどの場面)
equalp: さらに緩い(大小無視、配列内容)

Q7. 並行性

標準にはない。bordeaux-threads がポータブル標準。SBCL強力

Q8. デフォルト引数

(defun greet (name &optional (greeting "Hello"))
  (format nil "~a, ~a!" greeting name))

&optional でデフォルト引数。

Q9. キーワード引数

(defun connect (&key (host "localhost") (port 8080))
  ...)
(connect :port 9000)

Q10. 非定型な残り引数

(defun sum (&rest nums) (apply #'+ nums))
(sum 1 2 3)    ;; 6

42. 学習ロードマップ詳細

Week 1: 基礎

  • SBCL + Quicklisp + Emacs/SLIME
  • S式、関数、変数
  • リスト、cons、car、cdr
  • 制御構造(if、cond、loop)

Week 2: 中級

  • defun / lambda
  • mapcar / reduce / filter
  • 文字列・ハッシュ
  • パッケージ

Week 3: マクロ

  • defmacro基礎
  • quasiquote / unquote
  • macroexpand
  • gensym

Week 4: CLOS

  • defclass / defmethod
  • 多重ディスパッチ
  • メソッドコンビネーション
  • 継承

Week 5: コンディション

  • handler-case
  • restart-case
  • 自作コンディション

Week 6: Web


応用: DSLと歴史


44. リーダマクロでの言語拡張

44-1. 標準のリーダマクロ

'(1 2 3)         ;; (quote (1 2 3))
`(1 ,a)          ;; (quasiquote (1 (unquote a)))
,@list           ;; (unquote-splicing list)
#'+              ;; (function +)
#(1 2 3)         ;; ベクタリテラル
#\a              ;; 文字
#:foo            ;; uninterned symbol
#+sbcl           ;; 条件付き読み込み
#-sbcl
#| ... |#         ;; ブロックコメント

44-2. 自作リーダマクロ

(set-dispatch-macro-character #\# #\$
  (lambda (stream char1 char2)
    (declare (ignore char1 char2))
    `(format nil "$~a" ,(read stream))))

#$"100"           ;; "$100"
#$x               ;; (format nil "$~a" x)

ソースコードのパース自体をカスタマイズ」できる、最強のメタプログラミング。


44-3. cl-interpol(文字列補間)

(named-readtables:in-readtable :interpol-syntax)

(let ((name "Alice"))
  #?"Hello, ${name}!")        ;; "Hello, Alice!"

文字列補間構文を後付け」できる。Lispの柔軟性の象徴。


44-4. このセクションのまとめ

- リーダマクロでパース拡張
- set-dispatch-macro-characterで自作
- cl-interpolで文字列補間
- 言語の構文自体をカスタマイズ可能

45. 自作DSLの実装例

45-1. SQL DSL

(defmacro select (columns from-clause where-clause)
  `(format nil "SELECT ~a FROM ~a WHERE ~a"
           ',columns ',from-clause ',where-clause))

(select (id name) users (= active t))
;; "SELECT (ID NAME) FROM USERS WHERE (= ACTIVE T)"

45-2. 状態機械DSL

(defmacro define-state-machine (name &body states)
  `(defun ,name (initial-state)
     (let ((state initial-state))
       (lambda (event)
         (case state
           ,@(mapcar
               (lambda (s)
                 `(,(car s) (case event ,@(cdr s))))
               states))))))

(define-state-machine traffic-light
  (red (timeout green))
  (green (timeout yellow))
  (yellow (timeout red)))

45-3. このセクションのまとめ

- マクロで任意のDSL
- SQL / HTML / 状態機械 / 設定言語
- Lispの真の力

46. Lispの歴史と文化

1958  John McCarthyが発明
1960論文「Recursive Functions...」
1962  LISP 1.5
1980年代 黄金時代(Lisp Machine、Symbolics、AI研究)
1986〜 Common Lisp標準化
1990年代 一般PCへの移行で衰退
2000年代Paul GrahamのWeb Application(ViaWeb)
継続    根強いコミュニティ、研究と少数派の業務利用

47. Common Lispの未来

活発な分野:
  - SBCLの継続改善
  - Quicklisp / ultralisp(パッケージ)
  - Web Assemblyターゲット(Coalton)
  - 静的型付けライブラリ(Coalton、Typed Common Lisp)
  - 教育(Practical Common Lispの遺産)

業務利用例:
  - Grammarly(一部)
  - ITA Software(Googleが買収)
  - Rigetti(量子コンピューティング)
  - Mind AI(韓国)
  - Roomba(iRobot、AI部分)

48. Common Lispの特徴

LispはOS」と言われる所以:

- REPLでプログラムを書きながら動かす
- 関数を再定義して即座に反映
- デバッガでエラーを対話的に修復
- コンパイル済みコードとREPLのシームレスな統合
- 自分のコードとLisp本体の境目がない

これは他言語にはない体験です。


発展: 実装モデル


50. Common Lispの評価モデル詳細

50-1. 評価ルールの正確な定義

;; 評価器の擬似コード
(defun eval (form env)
  (cond
    ((symbolp form)              ; シンボル → 値を取り出す
     (lookup form env))
    ((atom form)                  ; 数値・文字列・配列 → そのまま
     form)
    ((listp form)
     (let ((operator (car form)))
       (cond
         ((special-operator-p operator)    ; 特殊形式
          (eval-special form env))
         ((macro-function operator)        ; マクロ
          (eval (macroexpand form) env))
         (t                                  ; 関数呼び出し
          (apply (lookup-function operator env)
                 (mapcar (lambda (arg) (eval arg env)) (cdr form)))))))))

50-2. 特殊形式の一覧

;; 標準特殊形式(25個)
quote, function, if, progn, let, let*, setq,
flet, labels, macrolet, symbol-macrolet,
catch, throw, unwind-protect,
block, return-from, tagbody, go,
multiple-value-call, multiple-value-prog1,
the, eval-when, locally, load-time-value,
declare, declaim

50-3. このセクションのまとめ

- 評価器は再帰的
- 25個の特殊形式
- マクロは展開してから評価
- 関数呼び出しは引数評価後

51. PC-Lisp Machineの遺産

1980年代のLisp Machine:
  - Symbolicsの3600シリーズ
  - LMI、TI Explorer
  - 「Lispで書かれたOS」
  - GUI、ネットワーク、デバッガ全部Lisp
  - 高価で消滅したが、文化を残した

現代への影響:
  - GNU EmacsのLisp環境
  - SLIMEのデバッガ
  - REPL駆動開発
  - 「言語がOS」の発想

52. 実用例:簡単なWebアプリ

(ql:quickload '(:hunchentoot :cl-who))

(defparameter *server* nil)

(setf *server* (hunchentoot:start
                (make-instance 'hunchentoot:easy-acceptor :port 8080)))

(hunchentoot:define-easy-handler (home :uri "/") ()
  (cl-who:with-html-output-to-string (s)
    (:html
      (:head (:title "Lisp Web"))
      (:body
        (:h1 "Hello from Common Lisp!")
        (:p "This is a Lisp Web app")))))

;; http://localhost:8080/ で確認

53. パターンマッチング(trivia / optima)

(ql:quickload :trivia)

(defun describe (value)
  (trivia:match value
    ((list 'point x y)
     (format t "Point at ~a, ~a~%" x y))
    ((list 'circle (list 'point x y) r)
     (format t "Circle at ~a, ~a, r=~a~%" x y r))
    ((list a b c)
     (format t "Triple: ~a ~a ~a~%" a b c))
    (_
     (format t "Unknown~%"))))

(describe '(point 1 2))                     ;; Point at 1, 2
(describe '(circle (point 0 0) 5))          ;; Circle at 0, 0, r=5

Haskell / Rust風のパターンマッチング」をマクロで実現。


54. 完全なテストフレームワーク使用

(ql:quickload :fiveam)

(fiveam:def-suite calculator-tests)
(fiveam:in-suite calculator-tests)

(fiveam:test addition
  "Addition should work"
  (fiveam:is (= 3 (+ 1 2)))
  (fiveam:is (= 0 (+ -1 1))))

(fiveam:test subtraction
  (fiveam:is (= 1 (- 3 2))))

(fiveam:run! 'calculator-tests)

実践: プロジェクト運用


56. 大規模プロジェクトの構造

my-large-project/
├── my-large-project.asd
├── README.md
├── LICENSE
├── docs/
├── src/
│   ├── package.lisp
│   ├── core/
│   │   ├── package.lisp
│   │   ├── types.lisp
│   │   ├── protocols.lisp
│   │   └── implementations.lisp
│   ├── api/
│   │   ├── package.lisp
│   │   ├── routes.lisp
│   │   └── handlers.lisp
│   ├── data/
│   │   ├── package.lisp
│   │   ├── models.lisp
│   │   └── repositories.lisp
│   ├── services/
│   └── main.lisp
├── tests/
├── scripts/
└── config/

57. ASDFのdepends-onパターン

(asdf:defsystem #:my-app
  :class :package-inferred-system
  :depends-on (:my-app/main)
  :pathname "src")

(register-system-packages :my-app/main '(:my-app))

package-inferred-systemファイル単位の依存関係を自動推論。モダンなアプローチ。


58. 実用パターン集(マクロ)

58-1. ガード句

(defmacro guard (condition error-msg &body body)
  `(if ,condition
       (progn ,@body)
       (error ,error-msg)))

(defun divide (x y)
  (guard (not (zerop y)) "Division by zero"
    (/ x y)))

58-2. lazy評価

(defmacro lazy (form)
  (let ((computed (gensym))
        (result (gensym)))
    `(let ((,computed nil)
           (,result nil))
       (lambda ()
         (unless ,computed
           (setf ,result ,form
                 ,computed t))
         ,result))))

(defparameter expensive (lazy (heavy-compute)))
(funcall expensive)    ;; 初回実行
(funcall expensive)    ;; キャッシュ済み

58-3. 観察者パターン

(defclass observable ()
  ((observers :initform nil)))

(defmethod add-observer ((subject observable) observer)
  (push observer (slot-value subject 'observers)))

(defmethod notify ((subject observable) event)
  (dolist (observer (slot-value subject 'observers))
    (funcall observer event)))

58-4. このセクションのまとめ

- ガード句マクロ
- lazy評価
- 観察者パターン
- マクロの実用度合いを示す例

59. Common Lispの現代的活用

- AI研究(自然言語処理、エキスパートシステム)
- 金融(Standard Charteredの一部)
- 暗号通貨(Mind AI)
- ロボット(一部)
- 数学処理系(Maxima)
- ドメイン特化DSL構築
- 個人プロジェクト・教育

60. CLコミュニティ

カンファレンス

  • ELS(European Lisp Symposium)
  • ICFP(関数型一般)

Web

書籍

  • 『Practical Common Lisp』Peter Seibel(無料)
  • 『On Lisp』Paul Graham(無料)
  • 『Let Over Lambda』Doug Hoyte
  • 『Land of Lisp』Conrad Barski

61. Common Lisp学習の進め方

Phase 1: 基礎(1ヶ月)
  - Practical Common Lispを読む
  - SBCL + SLIME環境
  - 簡単なスクリプト

Phase 2: 中級(2ヶ月)
  - On Lispでmacro
  - ASDFでプロジェクト
  - Quicklispで依存

Phase 3: 上級(3ヶ月+)
  - CLOSマスター
  - 自作DSL
  - Webアプリ
  - 自作ライブラリ公開

上級: 処理系と型


63. SBCLの内部構造

SBCLの構成:
  - リーダ(テキスト → S式)
  - コンパイラ(S式 → IR → 機械語)
  - GC(世代別 + コピー)
  - スレッドサポート
  - 標準ライブラリ

SBCLは、Common Lispのコードを段階的に内部表現へ落とし込み、最終的に機械語へコンパイルする処理系である。REPLで即座に式を評価できる一方で、関数単位ではかなり本格的な最適化を行う。つまり「対話的に書けるスクリプト環境」と「最適化コンパイラ」の両方の性格を持っている。

性能を見るときは、単に speed 宣言を足すだけでは不十分である。型宣言、配列の要素型、境界チェック、アロケーション、ジェネリック関数呼び出しの頻度、クロージャ生成などが複合して効く。SBCLは警告や型推論結果を比較的よく出してくれるため、コンパイラからのメッセージを読むことがチューニングの入口になる。

内部構造を理解する利点は、デバッグにもある。未定義関数、型不一致、最適化に伴うデバッグ情報の欠落、GCによる停止時間などは、処理系の動作を知っているほど原因を切り分けやすい。SBCLを使うなら、マニュアルのコンパイラ、スレッド、GC、プロファイラの章は一度通読しておく価値がある。


64. 高度な型システム

;; 型宣言
(declaim (ftype (function (integer integer) integer) my-add))

(defun my-add (x y)
  (declare (type integer x y)
           (optimize speed))
  (+ x y))

;; 型クエリ
(typep 42 'integer)         ;; T
(subtypep 'integer 'number)  ;; T, T

Common Lispの型は、静的型付け言語の型とは少し役割が違う。型宣言は主にコンパイラへの情報提供、実行時検査、ドキュメント、最適化のために使う。型宣言を書いても、すべての誤りがコンパイル時に止まるわけではない。その代わり、必要な場所にだけ型情報を足せる柔軟さがある。

特に数値計算や配列処理では、型宣言が性能に直結する。integer より fixnum、一般的な vector より要素型を指定した配列、抽象的な number より single-floatdouble-float のほうが、コンパイラが効率的なコードを生成しやすい。ただし、型宣言は読みやすさを落とすこともあるため、まず正しく書き、プロファイルしてから熱い箇所に足すのがよい。

型システムは設計の語彙にもなる。deftype でドメイン固有の型名を与えれば、関数の意図を表現しやすくなる。たとえば「ユーザーID」「非空リスト」「正の整数」のような名前は、コメントよりもコードの形として残りやすい。


65. CFFI(C関数呼び出し)

(ql:quickload :cffi)

(cffi:define-foreign-library libcurl
  (:darwin "libcurl.dylib")
  (:unix "libcurl.so")
  (t (:default "libcurl")))

(cffi:use-foreign-library libcurl)

(cffi:defcfun "curl_easy_init" :pointer)

C関数をLispから呼べる。POSIX、OpenGL、データベースなど活用範囲広い。

CFFIを使うと、Common Lispの外側にある巨大な資産へ接続できる。ただし、境界を越える部分ではLispの安全さや対話性がそのまま保たれるわけではない。ポインタ、メモリ解放、文字列エンコーディング、構造体レイアウト、スレッド安全性は、C側の規約に従って扱う必要がある。

小さく始めるなら、最初は値を返すだけの単純な関数から包む。次にエラーコード、リソース解放、コールバック、構造体を扱う。外部ライブラリの生APIをアプリケーション全体に広げず、Lispらしい薄いラッパーを作って境界を閉じると、テストしやすくなる。

FFIは便利だが、移植性のコストも生む。OSごとのライブラリ名、アーキテクチャごとのABI差、配布時の共有ライブラリ配置を事前に決めておく。Common Lispだけで完結する部分と、Cへ委ねる部分の境界を明確にすることが、長く保守できるFFIコードの条件である。


43-A. 実践的な型システムと制約

Common Lisp の型系は optional ですが、適切に使うと性能と安全性が上がります。

型宣言(Type Declaration)

(defun fast-sum (numbers)
  (declare (type (vector fixnum) numbers)
           (optimize (speed 3) (safety 0)))
  (let ((sum 0))
    (declare (type fixnum sum))
    (dotimes (i (length numbers))
      (setf sum (+ sum (aref numbers i))))
    sum))

(fast-sum #(1 2 3 4 5))  ; => 15

最適化フラグ:

  • (speed 3) : 速度最優先
  • (safety 0) : 安全性チェック無し(エラーが起きても責任を持たない)
  • (compilation-speed 3) : コンパイル高速化

type の例

; 基本型
(declare (type integer x))
(declare (type string name))
(declare (type list items))

; 複合型
(declare (type (vector fixnum 1000) fast-array))  ; サイズ1000の fixnum ベクタ
(declare (type (or integer float) number))         ; integer または float
(declare (type (function (integer integer) integer) my-func))  ; 関数型

43-B. ASDF とプロジェクト構成

Common Lisp の実務では ASDF(Another System Definition Facility)が必須です。

system 定義

; myproject.asd
(asdf:defsystem "myproject"
  :description "My project"
  :author "Author Name"
  :license "MIT"
  :version "0.1.0"
  :depends-on ("alexandria" "iterate" "log4cl")
  :components
  ((:module "src"
    :components
    ((:file "package")
     (:file "utils" :depends-on ("package"))
     (:file "core" :depends-on ("utils"))
     (:file "api" :depends-on ("core")))))
  :in-order-to ((test-op (test-op "myproject/tests"))))

(asdf:defsystem "myproject/tests"
  :depends-on ("myproject" "fiveam")
  :components
  ((:module "tests"
    :components
    ((:file "test-utils")
     (:file "test-core" :depends-on ("test-utils")))))
  :perform (test-op (o s)
    (uiop:symbol-call :fiveam :run-all-tests)))

使用:

(asdf:load-system "myproject")
(asdf:test-system "myproject")

43-C. マクロの高度な技法

Anaphoric マクロ(暗黙的な変数を導入)

(defmacro aif (test then &optional else)
  "Anaphoric if: it 変数が test の結果にバインドされる"
  `(let ((it ,test))
     (if it ,then ,else)))

(aif (find-user "alice")
     (format t "Found: ~A~%" (user-name it))
     (format t "Not found~%"))

Macro を返すマクロ

(defmacro define-accessor (name field)
  "accessor function を自動生成"
  `(defmacro ,name (obj)
     `(slot-value ,obj ',',field)))

(define-accessor user-name user-obj)

(user-name my-user)  ; (slot-value my-user 'user-obj)

43-D. マルチスレッド・並行処理

(defpackage #:concurrent-example
  (:use #:cl #:bordeaux-threads))

(in-package :concurrent-example)

(defvar *lock* (make-lock))
(defvar *counter* 0)

(defun increment-counter ()
  (with-lock-held (*lock*)
    (incf *counter*)))

(defun demo ()
  (loop repeat 5
        collect (make-thread
                  (lambda ()
                    (loop repeat 100 do (increment-counter))))))

(defun run ()
  (setf *counter* 0)
  (dolist (thread (demo))
    (join-thread thread))
  (format t "Final counter: ~A~%" *counter*))  ; 500

43-E. Web フレームワーク(Hunchentoot / Clack)

Hunchentoot の基本

(defpackage #:web-example
  (:use #:cl #:hunchentoot))

(in-package :web-example)

(defvar *server* nil)

(define-easy-handler (hello :uri "/hello") ()
  (with-html-output-to-string (s)
    (:html
      (:body
        (:h1 "Hello, World!")))))

(define-easy-handler (greet :uri "/greet") (name)
  (with-html-output-to-string (s)
    (:html
      (:body
        (:h1 "Hello, " (:princ name) "!")))))

(defun start-server ()
  (setf *server* (start (make-instance 'easy-acceptor :port 8080)))
  (format t "Server started on http://localhost:8080~%"))

(defun stop-server ()
  (stop *server*))

; 実行
; (start-server)
; http://localhost:8080/hello?name=Alice

まとめ

Common Lispは、マクロ、REPL、動的な開発体験、強力な条件システムを備えた、探索的なプログラミングに向いた言語です。構文の少なさだけでなく、言語そのものを拡張しながら問題領域に近い表現を作れる点に特徴があります。

参考文献

書籍

解説・補助