CSV分析ツールを作りながら学ぶ、
Common Lisp入門
(SBCL)

  • Common LispのSBCLを使い、CSVファイルを読み込んで集計・検索するコマンドラインツールを段階的に作る入門記事です。
  • S式・defun・loop・mapcarなどの基本構文を、実際に動くコードを通じて学べます。
  • ハッシュテーブルによるグループ集計や、高階関数を使ったフィルタリングなど実用的な機能を実装します。
  • キーワード引数・多値返却・destructuringといった、他言語にはないLisp固有の設計も解説しています。

関連記事

1. はじめに

Common Lispの基本的な使い方を身につけることを目標に、具体的なCSVファイルの集計・検索のコマンドラインツールを作ってみます。
ファイルを読んで計算するだけで何も壊さないので、安心して試行錯誤できます。

Common Lispは1984年に登場した汎用プログラミング言語です1
動的型付け、ガベージコレクション、対話的なREPL環境を備えています。
Lispは、もともと1960年に設計され、List Processingに由来する通り、データもプログラムも同じリスト構造で表現するという設計です2
Common Lispは、その一つの派生言語ということになります。

1.1. 環境の準備

処理系はSBCL(Steel Bank Common Lisp)を使います3
無料で、最も広く使われています。

# Windows
# sbcl.org からインストーラをダウンロード

# macOS
brew install sbcl

# Ubuntu / Debian
sudo apt install sbclCode language: PHP (php)

SBCLを起動するとREPLが立ち上がります。
REPLとはRead-Eval-Print Loopの略で、コードを入力すると即座に評価して結果を返す対話環境です4

$ sbcl
* (+ 1 2)
3

*がSBCLのプロンプトです。
式を打ってEnterを押すと結果がすぐ返る。
このフィードバックの速さがLispの開発体験の核にあります。
コード例はSBCL 2.2以降で動作確認しました。

1.2. サンプルCSVを用意する

まずは、サンプルCSVを用意しておきましょう。
中身は架空の売上データです。

date,product,category,quantity,price
2025-01-05,りんご,果物,10,150
2025-01-05,にんじん,野菜,5,80
2025-01-06,バナナ,果物,8,120
2025-01-06,トマト,野菜,12,100
2025-01-07,りんご,果物,6,150
2025-01-07,キャベツ,野菜,3,200
2025-01-08,バナナ,果物,15,120
2025-01-08,にんじん,野菜,7,80

これを作業用のディレクトリ内(たとえば、~/playground)にsales.csvとして保存してください。

2. ファイルを読んで表示する

S式:すべてはこの形 前置記法 (+ 1 2) ; => 3 (* 3 (+ 4 5)) (string-upcase “hi”) 関数名が括弧の先頭 優先順位を覚えなくていい すべてがS式 nil と真偽値 nil = 偽 かつ 空リスト nil 以外はすべて真 0 → 真 “” → 真 C / JS とは異なる ‘(+ 1 2) → データ (+ 1 2) → 命令

2.1. S式 ── すべてはこの形

REPLに打ち込むところから始めます。

(+ 1 2)              ; => 3
(* 3 (+ 4 5))        ; => 27
(string-upcase "hi") ; => "HI"Code language: PHP (php)

1 + 2ではなく(+ 1 2)
関数名が括弧の先頭に来て、引数がその後に続く。
これを前置記法と呼びます。
(* 3 (+ 4 5))は他の言語の3 * (4 + 5)に相当しますが、括弧の入れ子だけで評価順序が決まるので、演算子の優先順位を覚える必要がありません5

この括弧で囲まれた形式をS式(Symbolic Expression)と呼びます。
関数呼び出しも変数定義も条件分岐も、すべてがS式です。

2.2. ファイルを1行ずつ読む

(with-open-file (stream "sales.csv" :direction :input)
  (loop for line = (read-line stream nil nil)
        while line
        collect line))Code language: JavaScript (javascript)

このコードを理解するために、各要素を見ていきます。

with-open-fileはファイルを開き、本体を実行し、終わったら自動で閉じます。
エラーが起きても閉じる。
Pythonのwith open(...) as f:と同じ目的のマクロです6
:direction :inputは読み込みモードの指定。

loopはCommon Lispの万能ループマクロです7
英語に近い構文を持ち、for line = (read-line stream nil nil)で「1行ずつ読んでlineに入れる」、while lineで「nilが返ったら(ファイル末尾に達したら)終了」、collect lineで「各行をリストとして集める」と読めます。

read-lineの第2引数nilは「ファイル末尾でエラーを出すな」、第3引数nilは「ファイル末尾でnilを返せ」という指定です。

結果はこうなります。

("date,product,category,quantity,price"
 "2025-01-05,りんご,果物,10,150"
 "2025-01-05,にんじん,野菜,5,80"
 ...)Code language: JavaScript (javascript)

文字列のリストが返ってきました。
リストはCommon Lispの中心的なデータ構造です。

2.3. データ型を眺める

ここまでに登場したデータを整理しておきます。

;; 文字列
"hello"

;; 整数 ── 桁数に制限がない
42
100000000000000000000000000000

;; 有理数 ── 分数をそのまま保持する(丸め誤差なし)8
(/ 1 3)       ; => 1/3
(+ 1/3 1/6)   ; => 1/2

;; 浮動小数点数
3.14

;; シンボル ── 変数名や関数名に使う識別子
;; 大文字小文字を区別しない(内部で大文字に変換される)
hello           ; シンボル HELLO

;; キーワードシンボル ── コロンで始まる。
常に自分自身に評価される
:name
:direction

;; 真偽
t               ; 真
nil             ; 偽(空リストでもある)

;; リスト
'(1 2 3)        ; クォート ' で評価を抑制してデータにする
(list 1 2 3)    ; list関数でも作れるCode language: PHP (php)

クォート'はコードとデータを区別する印です。
(+ 1 2)は「1と2を足せ」という命令で結果は3。
'(+ 1 2)は「+と1と2の3要素からなるリスト」というデータです。

nilが「偽」と「空リスト」の両方を兼ねるのはLispの歴史的な設計で、条件判定とリストの終端処理がうまく噛み合います。
nil以外はすべて真。
0も空文字列も真になる。CやJavaScriptとはここが違います9

3. CSVを行ごとに分割する ── 関数とリスト操作

CSV行をカンマで区切って、フィールドのリストにしたい。

CSVを行ごとに分割する ファイルを開く with-open-file 自動でclose エラーでも安全 :direction :input 1行ずつ読む loop + read-line while line で終了 collect で収集 nil → 末尾シグナル カンマで分割 split-by-comma push → nreverse O(1) で高速収集 文字列リストを返す (“date” “product” “category” “quantity” “price”) ; ヘッダ行のリスト

3.1. 関数を定義する

(defun split-by-comma (line)
  "カンマ区切りの文字列をリストに分割する。
"
  (let ((result '())
        (start 0))
    (loop for i from 0 below (length line)
          when (char= (char line i) #\,)
          do (push (subseq line start i) result)
             (setf start (1+ i)))
    (push (subseq line start) result)
    (nreverse result)))

(split-by-comma "2025-01-05,りんご,果物,10,150")
;; => ("2025-01-05" "りんご" "果物" "10" "150")Code language: PHP (php)

defunで関数を定義します。
第1引数が関数名、第2引数がパラメータリスト、文字列がドキュメント文字列、残りが本体。
本体の最後の式の値が自動的に戻り値です。

letはローカル変数を作ります。
resultは空リスト、startは0から始まります。

pushはリストの先頭に要素を追加します。
末尾に追加するより速い(O(1))ので、Common Lispではpushで集めて最後にnreverseで逆順にするのが定番パターンです10

setfは汎用の代入マクロです。
変数だけでなく、配列の要素やハッシュテーブルの値など「場所」に対して使えます。

char=は文字同士の比較、#\,はカンマの文字リテラル。
subseqは部分文字列の切り出しです。

3.2. リスト操作の基本

CSVのデータはリストとして扱うので、リスト操作を押さえましょう。

(first '("a" "b" "c"))     ; => "a"(先頭要素)
(rest '("a" "b" "c"))      ; => ("b" "c")(先頭以外)
(nth 2 '("a" "b" "c"))     ; => "c"(0始まり)
(length '(1 2 3))           ; => 3
(append '(1 2) '(3 4))      ; => (1 2 3 4)
(reverse '(1 2 3))          ; => (3 2 1)Code language: PHP (php)

firstrestには歴史的な別名carcdrがあります。IBMのコンピュータのレジスタ名に由来する名前で、今でもよく使われます11

3.3. CSVを全行パースする

(defun read-csv (path)
  "CSVファイルを読み、ヘッダとデータ行のリストを返す。
"
  (with-open-file (stream path :direction :input)
    (let* ((header (split-by-comma (read-line stream)))
           (rows (loop for line = (read-line stream nil nil)
                       while line
                       collect (split-by-comma line))))
      (values header rows))))Code language: PHP (php)

let*letと似ていますが、先に定義した変数を後の変数で参照できます。
headerを読んでからrowsを読む、という順序が自然に書けます。

valuesは複数の値を返す関数です。
ヘッダとデータ行を別々に返しています。
受け取る側はこう書きます。

(multiple-value-bind (header rows) (read-csv "sales.csv")
  (format t "カラム: ~{~A~^, ~}~%" header)
  (format t "~D行のデータ~%" (length rows)))
;; カラム: date, product, category, quantity, price
;; 8行のデータCode language: JavaScript (javascript)

multiple-value-bindで複数の返り値を別々の変数に束縛します。
多値はタプルやリストと違い、受け取り側が気にしなければ第1値だけが使われて残りは捨てられます。余分なメモリ確保が起きません12

format~{~A~^, ~}はリストをカンマ区切りで表示する指示子です13
~{~}の間がリストの各要素に繰り返し適用され、~^は最後の要素ではセパレータを出しません。

4. データを絞り込む ── 条件分岐と高階関数

特定のカテゴリだけ、特定の日付だけ、といったフィルタリングを実装します。

データを絞り込む 高階関数 remove-if-not 条件に合う行だけ残す(フィルタ) mapcar 各要素を変換して新リストを作る reduce リストを1つの値にまとめる mapcar → reduce のパイプライン lambda と #’ (lambda (row) (nth 1 row)) 名前なし関数をその場で定義 #’evenp 関数オブジェクトを取り出す記法 関数と変数の名前空間が別 Lisp-2 設計

4.1. 条件分岐

;; if ── 真なら第2式、偽なら第3式
(if (> 3 2) "yes" "no")   ; => "yes"

;; when ── 真のときだけ実行。
暗黙のprognで複数の式を書ける
(when (> 3 2)
  (print "calculating")
  "done")

;; unless ── 偽のときだけ実行
(unless (> 2 3)
  "2 is not greater")

;; cond ── 複数の条件を上から順に試す
(defun categorize-amount (amount)
  (cond
    ((>= amount 1000) "高額")
    ((>= amount 500)  "中額")
    (t                 "少額")))Code language: PHP (php)

ifの各枝には式を1つしか書けません。
複数の処理を並べたい場合はwhenを使うか、prognで包みます。

4.2. 高階関数でフィルタリング

関数を引数に取る関数を高階関数と呼びます。
リスト処理の主力です。

;; remove-if-not ── 条件に合わない要素を取り除く(= フィルタ)
(remove-if-not #'evenp '(1 2 3 4 5))
;; => (2 4)Code language: PHP (php)

#'evenpは「evenpという関数オブジェクトを取り出す」記法です。
Common Lispでは関数と変数の名前空間が分かれているため、関数を値として渡すには#'が必要です14

lambdaを使えば、名前のない関数をその場で定義できます。

;; lambdaでフィルタ関数を書く
(remove-if-not (lambda (row)
                 (string= (nth 2 row) "果物"))
               rows)
;; カテゴリが「果物」の行だけ残るCode language: JavaScript (javascript)

CSVツールのフィルタ関数を作りましょう。

(defun filter-rows (rows column-index value)
  "指定カラムの値がVALUEに等しい行だけを返す。
"
  (remove-if-not (lambda (row)
                   (string= (nth column-index row) value))
                 rows))Code language: PHP (php)
(multiple-value-bind (header rows) (read-csv "sales.csv")
  (let ((fruits (filter-rows rows 2 "果物")))
    (dolist (row fruits)
      (format t "~{~A~^, ~}~%" row))))
;; 2025-01-05, りんご, 果物, 10, 150
;; 2025-01-06, バナナ, 果物, 8, 120
;; ...Code language: JavaScript (javascript)

dolistはリストの各要素について繰り返す構文です。
他の言語のfor-eachに相当します。

4.3. mapcar ── リストを変換する

;; 各行から商品名(インデックス1)だけ取り出す
(mapcar (lambda (row) (nth 1 row)) rows)
;; => ("りんご" "にんじん" "バナナ" "トマト" ...)

;; 数量(インデックス3)を数値に変換する
(mapcar (lambda (row) (parse-integer (nth 3 row))) rows)
;; => (10 5 8 12 6 3 15 7)Code language: PHP (php)

mapcarは各要素に関数を適用して新しいリストを作ります15
他の言語のmapに相当します。

4.4. reduce ── リストを1つの値にまとめる

;; 数量の合計
(reduce #'+ (mapcar (lambda (row) (parse-integer (nth 3 row))) rows))
;; => 66

;; 売上金額の合計
(reduce #'+
  (mapcar (lambda (row)
            (* (parse-integer (nth 3 row))
               (parse-integer (nth 4 row))))
          rows))
;; => 7920Code language: PHP (php)

mapcarで変換してからreduceで集約する。
このパイプラインはCommon Lispで頻繁に使うパターンです。

5. 集計機能を作る ── ハッシュテーブルとformat

カテゴリ別の集計、商品別の集計を実装します。

集計機能を作る group-by ハッシュテーブル :test #’equal 文字列比較が必要 果物 → [行A,行B] 野菜 → [行C,行D] summarize funcall 関数を引数として渡す 汎用的に集計できる 集計関数を外から注入 sort で結果を整列 format で整形 ~VA 幅指定で揃えて出力 カテゴリ | 売上 ——– + —– 果物 | 5160 group-by → summarize(funcall) → format の3ステップで集計完成

5.1. ハッシュテーブル

キーと値の組を大量に管理するにはハッシュテーブルを使います。

(defun group-by (rows column-index)
  "指定カラムの値でグループ化する。
   キーがカラム値、値が行のリストのハッシュテーブルを返す。
"
  (let ((groups (make-hash-table :test #'equal)))
    (dolist (row rows groups)
      (let ((key (nth column-index row)))
        (push row (gethash key groups '()))))))Code language: PHP (php)

make-hash-tableでハッシュテーブルを作ります。
:test #'equalは「文字列の内容で比較する」指定です。デフォルトの#'eqlでは文字列が同じ内容でも別オブジェクトなら一致しません16

gethashはキーに対応する値を返します。
第3引数はキーが存在しないときのデフォルト値です。
(push row (gethash key groups '()))は、キーに対応するリストの先頭にrowを追加する。
キーが存在しなければ空リストから始まります。

dolistの第3引数groupsは、ループが終わった後に返す値です。

(multiple-value-bind (header rows) (read-csv "sales.csv")
  (let ((by-category (group-by rows 2)))
    (maphash (lambda (key group)
               (format t "~A: ~D件~%" key (length group)))
             by-category)))
;; 果物: 4件
;; 野菜: 4Code language: JavaScript (javascript)

maphashはハッシュテーブルの全要素を走査します。

5.2. 集計関数を汎用的に作る

(defun summarize (rows column-index value-fn)
  "指定カラムでグループ化し、各グループにVALUE-FNを適用した結果を返す。
"
  (let ((groups (group-by rows column-index))
        (result '()))
    (maphash (lambda (key group)
               (push (list key (funcall value-fn group)) result))
             groups)
    (sort result #'string< :key #'first)))Code language: PHP (php)

funcallは変数に入った関数を呼び出す関数です。
(value-fn group)とは書けません。Common Lispでは関数と変数の名前空間が別なので、変数の中の関数を呼ぶにはfuncallが必要です17

この名前空間の分離はLisp-2と呼ばれる設計で、関数名と変数名が衝突しないという利点があります。
listという関数を使いながらlistという変数を持てるのはこのおかげです。

;; カテゴリ別の売上合計
(multiple-value-bind (header rows) (read-csv "sales.csv")
  (summarize rows 2
    (lambda (group)
      (reduce #'+
        (mapcar (lambda (row)
                  (* (parse-integer (nth 3 row))
                     (parse-integer (nth 4 row))))
                group)))))
;; => (("果物" 5160) ("野菜" 2760))

;; 商品別の数量合計
(multiple-value-bind (header rows) (read-csv "sales.csv")
  (summarize rows 1
    (lambda (group)
      (reduce #'+ (mapcar (lambda (r) (parse-integer (nth 3 r))) group)))))
;; => (("キャベツ" 3) ("にんじん" 12) ("トマト" 12)
;;     ("バナナ" 23) ("りんご" 16))Code language: PHP (php)

5.3. formatで整形出力

結果を見やすく表示しましょう。

(defun print-table (header data &key (widths '(15 10)))
  "表形式で表示する。
"
  (format t "~{~VA~^ | ~}~%" (interleave widths header))
  (format t "~{~V,,,'-A~^ + ~}~%" (interleave widths header))
  (dolist (row data)
    (format t "~{~VA~^ | ~}~%" (interleave widths row))))

(defun interleave (widths items)
  "幅とアイテムを交互に並べる。
(w1 item1 w2 item2 ...)"
  (loop for w in widths
        for item in items
        collect w collect item))Code language: PHP (php)

~VAは「V文字分の幅で右に空白を埋めて出力する」指示子です。
Vはformatの引数リストから幅を取る記法。

(print-table '("カテゴリ" "売上合計")
             '(("果物" 5160) ("野菜" 2760)))
;; カテゴリ        | 売上合計
;; --------------- + ----------
;; 果物            | 5160
;; 野菜            | 2760Code language: PHP (php)

5.4. ここまでをまとめる

(defun csv-report (path group-column)
  "CSVを読み込み、指定カラムで集計して表示する。
"
  (multiple-value-bind (header rows) (read-csv path)
    (let* ((col-index (position group-column header :test #'string=))
           (quantity-index (position "quantity" header :test #'string=))
           (price-index (position "price" header :test #'string=)))
      (unless col-index
        (format t "カラム '~A' が見つかりません~%" group-column)
        (return-from csv-report nil))
      (let ((summary
              (summarize rows col-index
                (lambda (group)
                  (list
                   (reduce #'+
                     (mapcar (lambda (r) (parse-integer (nth quantity-index r)))
                             group))
                   (reduce #'+
                     (mapcar (lambda (r)
                               (* (parse-integer (nth quantity-index r))
                                  (parse-integer (nth price-index r))))
                             group)))))))
        (print-table (list group-column "数量" "売上")
                     (mapcar (lambda (s)
                               (list (first s)
                                     (first (second s))
                                     (second (second s))))
                             summary)
                     :widths '(15 8 10))))))

(csv-report "sales.csv" "category")
;; category        | 数量     | 売上
;; --------------- + -------- + ----------
;; 果物            | 39       | 5160
;; 野菜            | 27       | 2760Code language: PHP (php)

positionはリスト中の要素の位置を返します18
:test #'string=で文字列比較を指定。
見つからなければnilを返します。

return-fromはブロックの途中から抜ける構文です。
defunは暗黙的に関数名と同じ名前のブロックを作るので、return-from csv-reportで関数から即座に戻れます。

6. コードをファイルにまとめる ── loadとキーワード引数

REPLで試してきたコードをファイルに保存して再利用できるようにしましょう。

ファイルに保存して再利用する load (load “csv-tool.lisp”) S式をそのままファイルに書く REPLと同じ形式 .lisp ファイル 引数の種類 &key キーワード引数 名前で渡すので順序不要 &optional 省略可能引数 デフォルト値を設定できる &rest 可変長引数 リストで受け取る / apply で展開 組み合わせて柔軟なAPIを作れる

6.1. Lispファイルの読み込み

上で書いた関数群をcsv-tool.lispとして保存し、REPLから読み込みます。

(load "csv-tool.lisp")Code language: JavaScript (javascript)

loadはLispファイルを読み込んで順に評価します19
ファイルの中身はREPLに打ち込むのと同じS式です。

6.2. キーワード引数で柔軟にする

(defun csv-search (path column value &key (separator #\,) case-insensitive)
  "CSVから指定カラムの値がVALUEに一致する行を検索する。
"
  (multiple-value-bind (header rows) (read-csv path)
    (let ((col-index (position column header :test #'string=)))
      (unless col-index
        (format t "カラム '~A' が見つかりません~%" column)
        (return-from csv-search nil))
      (let ((matches (remove-if-not
                      (lambda (row)
                        (let ((cell (nth col-index row)))
                          (if case-insensitive
                              (string-equal cell value)
                              (string= cell value))))
                      rows)))
        (format t "~D件ヒット:~%" (length matches))
        (dolist (row matches)
          (format t "  ~{~A~^, ~}~%" row))
        matches))))

(csv-search "sales.csv" "product" "りんご")
;; 2件ヒット:
;;   2025-01-05, りんご, 果物, 10, 150
;;   2025-01-07, りんご, 果物, 6, 150Code language: PHP (php)

&key以降はキーワード引数です。
呼び出し側で名前を指定して渡せるので、順序を気にする必要がありません。
case-insensitiveのデフォルトはnil(偽)です。

;; &optional ── 位置で渡す省略可能な引数
(defun greet (name &optional (greeting "Hello"))
  (format nil "~A, ~A!" greeting name))

(greet "Taro")          ; => "Hello, Taro!"
(greet "Taro" "Hi")     ; => "Hi, Taro!"

;; &rest ── 可変長引数をリストとして受け取る
(defun sum-all (&rest numbers)
  (apply #'+ numbers))

(sum-all 1 2 3 4 5)    ; => 15Code language: PHP (php)

applyは関数をリストの要素に適用します。
(apply #'+ '(1 2 3))(+ 1 2 3)と同じです。

7. ソートと高度なフィルタ ── sort、loop、文字列操作

ソートと高度なフィルタ sort 破壊的ソート 元リストを直接変更する ⚠ copy-list で保護 n系関数と異なり名前で判別不可 (sort (copy-list rows) #’> :key #’revenue) loop の destructuring (loop for (date product category qty price) in rows …) nth の連発を避けられる loop の集約キーワード sum・count・maximize finally return で結果を返す 1ループで複数集計が可能

7.1. ソート

;; 売上金額の降順でソート
(defun sort-by-revenue (rows quantity-index price-index)
  (sort (copy-list rows)
        #'>
        :key (lambda (row)
               (* (parse-integer (nth quantity-index row))
                  (parse-integer (nth price-index row))))))Code language: PHP (php)

sortは破壊的にリストを変更します20
元の順序が必要ならcopy-listでコピーしてからソートしてください。
Common Lispではnで始まる関数(nreversenconcなど)が破壊的版ですが、sortは例外的に名前で判別できないので注意が必要です。

7.2. loopの詳しい使い方

CSVツールのコードではloopを多用してきました。
もう少し機能を見ておきましょう。

;; 数値の範囲
(loop for i from 1 to 5 collect (* i i))
;; => (1 4 9 16 25)

;; 集約を同時に行う
(loop for row in rows
      for qty = (parse-integer (nth 3 row))
      sum qty into total
      count (> qty 10) into big-orders
      maximize qty into max-qty
      finally (return (list total big-orders max-qty)))
;; => (66 2 15)

;; when / unless でフィルタ
(loop for row in rows
      when (string= (nth 2 row) "果物")
      collect (nth 1 row))
;; => ("りんご" "バナナ" "りんご" "バナナ")

;; destructuring(分解束縛)
(loop for (date product category qty price) in rows
      when (string= category "果物")
      collect (list product (* (parse-integer qty) (parse-integer price))))
;; => (("りんご" 1500) ("バナナ" 960) ("りんご" 900) ("バナナ" 1800))Code language: PHP (php)

loopのdestructuringは行のフィールドに名前をつけられるので、nthの連発より読みやすくなります。

7.3. 文字列操作

(length "hello")                ; => 5
(subseq "hello world" 0 5)      ; => "hello"
(concatenate 'string "a" "-" "b") ; => "a-b"
(string-upcase "hello")         ; => "HELLO"
(string-downcase "HELLO")       ; => "hello"
(search "ll" "hello")           ; => 2
(string= "abc" "abc")           ; => T(大文字小文字を区別)
(string-equal "Abc" "abc")      ; => T(区別しない)

;; 文字の置換
(substitute #\- #\Space "hello world") ; => "hello-world"Code language: PHP (php)

concatenateの第1引数は結果の型です。
'stringで「文字列として返せ」と指定します。

  1. 正確にはGuy L. Steele Jr.による書籍『Common Lisp the Language』が1984年に出版され事実上の標準となり、ANSI標準(X3.226-1994)として正式に承認されたのは1994年12月8日です。 – X3J13 – Wikipedia
  2. McCarthyの1960年の論文「Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I」がLispの理論的基礎を確立しました。 – Professor John McCarthy | Stanford Computer Science
  3. SBCLはCMU Common Lispからのフォークで、ソースコードから再現可能なビルドを重視しています。2026年2月時点の最新版は2.6.2です。 – About – Steel Bank Common Lisp
  4. Emacsベースの開発環境SLIME(Superior Lisp Interaction Mode for Emacs)やVim向けのSlimvを使うと、エディタとREPLが統合されてさらに快適になります。 – Getting Started | Common Lisp
  5. McCarthyは当初M式(M-expression)という中置記法も提案していましたが、プログラマがS式を直接使うことを好んだため定着しませんでした。 – Lisp (programming language) – Wikipedia
  6. with-open-fileの内部ではunwind-protectを使ってファイルの自動クローズを保証しています。同様のwith-系マクロはCommon Lispに多数あり、with-output-to-string、with-hash-table-iterator等があります。
  7. loopマクロはANSI標準の第6章で規定されています。loopのキーワード構文を好まないプログラマはiterate(ライブラリ)やseries(遅延リスト風ライブラリ)を代替として使うこともあります。
  8. Common Lispの数値タワーはinteger、ratio、float、complexの4階層からなり、ANSI標準の第12章で規定されています。 - [Common Lisp HyperSpec](https://www.lispworks.com/documentation/common-lisp.html)
  9. Schemeでは#fが偽を表し、空リスト'()とは別のオブジェクトです。Common LispでnilとTが自己評価するシンボルであることはANSI仕様のSection 1.4.1.4.7で定められています。
  10. pushはマクロで、(push item place)は(setf place (cons item place))に展開されます。nreverseの「n」は「non-consing」の略で、新しいコンスセルを作らず破壊的にリストを逆転させます。 – The Common Lisp Cookbook – Data Structures
  11. carはContents of the Address part of Register、cdrはContents of the Decrement part of Registerの略で、IBM 704の機械語命令に対応しています。Steve RussellがIBM 704上で最初のLispインタプリタを実装しました。 – Lisp (programming language) – Wikipedia
  12. 多値はSBCLではスタック上で受け渡されるため、リストやベクタを返す場合と違ってヒープ割り当てが発生しません。
  13. Common Lispのformat指示子は全部で約30種類あり、ANSI標準の第22章に詳細が記載されています。~Rで数を英語で綴ったり(forty-two)、~:Pで複数形のsを出し分けたりもできます。
  14. この設計はLisp-2と呼ばれます。Scheme(Lisp-1)とCommon Lisp(Lisp-2)の名前空間設計の議論は、Richard P. GabrielとKent Pitmanの1988年の論文「Technical Issues of Separation in Function Cells and Value Cells」で詳しく比較されています。 – Common Lisp – Wikipedia
  15. mapcarの「car」は各リストのcar(先頭要素)に関数を適用することに由来します。cdr側を処理するmaplistや、結果を返さずに副作用だけ実行するmapcもあります。
  16. Common Lispの等価性判定は4段階あります。eqはオブジェクトの同一性、eqlはeqに加え数値と文字の値比較、equalは構造的等価、equalpはequalに加え大文字小文字を無視する文字列比較と数値の型を超えた比較を行います。
  17. 引数がリストにまとまっている場合はapplyを使います。
    (funcall fn a b)と(apply fn (list a b))は同じ結果になります。
  18. positionはシーケンス汎用関数で、リストだけでなくベクタや文字列にも使えます。
    :from-end tを指定すると末尾から検索し、:start/:endで範囲を限定できます。
  19. loadはインタプリタ的に逐次評価しますが、compile-fileでファイルをコンパイルしてからloadすると実行速度が向上します。
    SBCLはloadの際にも内部的にコンパイルを行います。
  20. stable-sortは等価な要素の元の順序を保証する安定ソートです。
    sortは安定性を保証しません。
    どちらも破壊的です。