0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Common Lisp ベーシックマスター Level 7 (データ構造)

Last updated at Posted at 2025-12-06

1. データ構造とは

プログラムでは、複数のデータをまとめて扱う必要がある。Common Lisp には、目的に応じた様々なデータ構造が用意されている。

データ構造 特徴 用途
連想リスト(alist) キーと値のペアのリスト 小規模な辞書、設定
プロパティリスト(plist) キーと値が交互に並ぶ シンボルの属性、オプション
ハッシュテーブル 高速な検索 大規模な辞書、キャッシュ
構造体(defstruct) 名前付きフィールド レコード、オブジェクト

2. 連想リスト(alist)

2-1. 連想リストとは

連想リスト(association list, alist) は、キーと値のペア(cons セル)を要素とするリストである。

常のリストは位置(インデックス)でアクセスするが、連想リストはキーでアクセスする。設定ファイル、オプション引数、小規模な辞書など、キーと値のペアを扱う場面で使われる。連想リストは「リストだけ」で作ることができ、特別な関数や構造を必要としない。

;; 連想リストの例
'((name . "Alice")
  (age . 25)
  (city . "Tokyo"))

2-2. 連想リストの作成

;; cons でペアを作り、list でまとめる
(list (cons 'name "Alice")
      (cons 'age 25)
      (cons 'city "Tokyo"))
;; → ((NAME . "Alice") (AGE . 25) (CITY . "Tokyo"))

;; ドット記法で直接書く
'((name . "Alice")
  (age . 25)
  (city . "Tokyo"))

2-3. assoc:キーで検索

assoc はキーに対応するペアを返す。

(defparameter *person*
  '((name . "Alice")
    (age . 25)
    (city . "Tokyo")))

;; キーで検索
(assoc 'name *person*)
;; → (NAME . "Alice")

(assoc 'age *person*)
;; → (AGE . 25)

;; 値だけ取り出す
(cdr (assoc 'name *person*))
;; → "Alice"

;; 存在しないキー
(assoc 'country *person*)
;; → NIL

2-4. acons と pairlis:要素の追加

;; acons:先頭に追加(元のリストは変更しない)
(acons 'email "alice@example.com" *person*)
;; → ((EMAIL . "alice@example.com") (NAME . "Alice") (AGE . 25) (CITY . "Tokyo"))

;; pairlis:複数のペアを一度に作成
(pairlis '(a b c) '(1 2 3))
;; → ((A . 1) (B . 2) (C . 3))

2-5. 連想リストの更新

連想リストは先頭から検索されるため、新しい値を先頭に追加すれば「更新」になる。

;; 年齢を更新(実際は先頭に新しいペアを追加)
(defparameter *person*
  (acons 'age 26 *person*))

(assoc 'age *person*)
;; → (AGE . 26)  ; 先頭の新しい値が見つかる

2-6. 文字列キーの検索

assoc はデフォルトで eql で比較する。文字列をキーにする場合は :test を指定する。

(defparameter *dict*
  '(("apple" . "りんご")
    ("banana" . "バナナ")
    ("orange" . "オレンジ")))

;; :test を指定しないと見つからない
(assoc "apple" *dict*)
;; → NIL

;; :test #'equal で文字列比較
(assoc "apple" *dict* :test #'equal)
;; → ("apple" . "りんご")

2-7. 連想リストの用途

  • 設定ファイルの読み込み
  • 小規模な辞書
  • 関数のオプション引数
  • 一時的なデータの関連付け

3. プロパティリスト(plist)

3-1. プロパティリストとは

プロパティリスト(property list, plist) は、キーと値が交互に並ぶフラットなリストである。

;; プロパティリストの例
'(:name "Alice" :age 25 :city "Tokyo")

連想リストとの違い:

項目 連想リスト プロパティリスト
構造 ((k1 . v1) (k2 . v2)) (k1 v1 k2 v2)
キー 任意 通常キーワード
用途 汎用辞書 オプション、属性

3-2. getf:値の取得

(defparameter *person*
  '(:name "Alice" :age 25 :city "Tokyo"))

;; 値を取得
(getf *person* :name)
;; → "Alice"

(getf *person* :age)
;; → 25

;; 存在しないキー(デフォルト値を指定可能)
(getf *person* :country)
;; → NIL

(getf *person* :country "Unknown")
;; → "Unknown"

3-3. setf と getf:値の更新

(defparameter *person*
  (list :name "Alice" :age 25))

;; 値を更新
(setf (getf *person* :age) 26)

*person*
;; → (:NAME "Alice" :AGE 26)

;; 新しいキーを追加
(setf (getf *person* :city) "Tokyo")

*person*
;; → (:CITY "Tokyo" :NAME "Alice" :AGE 26)

3-4. remf:キーの削除

(defparameter *person*
  (list :name "Alice" :age 25 :city "Tokyo"))

;; キーを削除
(remf *person* :city)

*person*
;; → (:NAME "Alice" :AGE 25)

3-5. シンボルのプロパティリスト

各シンボルには固有のプロパティリストがある。getsetf でアクセスする。

;; シンボル 'apple にプロパティを設定
(setf (get 'apple 'color) 'red)
(setf (get 'apple 'taste) 'sweet)

;; プロパティを取得
(get 'apple 'color)
;; → RED

(get 'apple 'taste)
;; → SWEET

;; シンボルの全プロパティを取得
(symbol-plist 'apple)
;; → (TASTE SWEET COLOR RED)

3-6. キーワード引数との関係

関数のキーワード引数は、内部的にプロパティリストとして扱われる。

(defun greet (&key name age)
  (format t "Hello, ~a! You are ~a years old.~%" name age))

;; キーワード引数で呼び出し
(greet :name "Alice" :age 25)
;; Hello, Alice! You are 25 years old.

;; 順序は自由
(greet :age 30 :name "Bob")
;; Hello, Bob! You are 30 years old.

4. ハッシュテーブル

4-1. ハッシュテーブルとは

ハッシュテーブル は、キーと値のペアを高速に検索できるデータ構造である。連想リストが端から順番に探すのに対し、ハッシュテーブルは一発で見つけることができる。

4-2. make-hash-table:作成

;; 空のハッシュテーブルを作成
(defparameter *ht* (make-hash-table))

;; 文字列をキーにする場合は :test を指定
(defparameter *dict* (make-hash-table :test #'equal))

4-3. gethash:値の取得と設定

(defparameter *ages* (make-hash-table))

;; 値を設定
(setf (gethash 'alice *ages*) 25)
(setf (gethash 'bob *ages*) 30)
(setf (gethash 'carol *ages*) 28)

;; 値を取得
(gethash 'alice *ages*)
;; → 25
;; → T(2番目の戻り値:キーが存在したか)

;; 存在しないキー
(gethash 'dave *ages*)
;; → NIL
;; → NIL(キーが存在しない)

4-4. 複数の戻り値の活用

gethash は2つの値を返す。2番目の値でキーの存在を確認できる。

;; gethash は2つの値を返す:(1) 値、(2) キーが存在したか
(multiple-value-bind (value found-p)
    (gethash 'alice *ages*)
  (if found-p
      (format t "Alice is ~a years old.~%" value)
      (format t "Alice not found.~%")))
;; Alice is 25 years old.

4-5. remhash:キーの削除

(remhash 'bob *ages*)
;; → T(削除成功)

(gethash 'bob *ages*)
;; → NIL
;; → NIL

4-6. maphash:全要素の走査

;; 全要素を表示
(maphash (lambda (key value)
           (format t "~a: ~a~%" key value))
         *ages*)
;; ALICE: 25
;; CAROL: 28

4-7. hash-table-count:要素数

(hash-table-count *ages*)
;; → 2

4-8. 文字列キーのハッシュテーブル

;; :test #'equal で文字列をキーに
(defparameter *dict* (make-hash-table :test #'equal))

(setf (gethash "apple" *dict*) "りんご")
(setf (gethash "banana" *dict*) "バナナ")

(gethash "apple" *dict*)
;; → "りんご"

4-9. 連想リストとの使い分け

条件 推奨
要素数が少ない(〜20程度) 連想リスト
要素数が多い ハッシュテーブル
頻繁に検索する ハッシュテーブル
データが不変 連想リスト
シリアライズが必要 連想リスト

5. 構造体(defstruct)

5-1. 構造体とは

構造体 は、名前付きのフィールドを持つデータ型である。C言語の struct に似ている。

5-2. defstruct:構造体の定義

;; person 構造体を定義
(defstruct person
  name
  age
  city)

これにより自動的に以下が生成される:

  • コンストラクタ make-person
  • アクセサ person-name, person-age, person-city
  • 述語 person-p
  • コピー関数 copy-person

5-3. インスタンスの作成

;; インスタンスを作成
(defparameter *alice*
  (make-person :name "Alice"
               :age 25
               :city "Tokyo"))

*alice*
;; → #S(PERSON :NAME "Alice" :AGE 25 :CITY "Tokyo")

5-4. アクセサ:フィールドの読み取り

(person-name *alice*)
;; → "Alice"

(person-age *alice*)
;; → 25

(person-city *alice*)
;; → "Tokyo"

5-5. フィールドの更新

;; setf でフィールドを更新
(setf (person-age *alice*) 26)

(person-age *alice*)
;; → 26

5-6. 述語:型の判定

(person-p *alice*)
;; → T

(person-p "not a person")
;; → NIL

5-7. デフォルト値の設定

(defstruct point
  (x 0)    ; デフォルト値 0
  (y 0))

(make-point)
;; → #S(POINT :X 0 :Y 0)

(make-point :x 10)
;; → #S(POINT :X 10 :Y 0)

5-8. 構造体の継承

;; 基底構造体
(defstruct animal
  name
  age)

;; 継承した構造体
(defstruct (dog (:include animal))
  breed)

(defparameter *pochi*
  (make-dog :name "Pochi"
            :age 3
            :breed "Shiba"))

;; 親のアクセサも使える
(animal-name *pochi*)
;; → "Pochi"

(dog-breed *pochi*)
;; → "Shiba"

;; 型の判定
(dog-p *pochi*)
;; → T

(animal-p *pochi*)
;; → T(dog は animal でもある)

5-9. 構造体とリストの使い分け

;; リストで表現(位置で意味を持たせる)
(defparameter *point-list* '(10 20))
(first *point-list*)   ; x
(second *point-list*)  ; y
;; → 何が x で何が y か分かりにくい

;; 構造体で表現
(defstruct point x y)
(defparameter *point-struct* (make-point :x 10 :y 20))
(point-x *point-struct*)
(point-y *point-struct*)
;; → 意図が明確

6. 実践例:住所録の実装

6-1. 構造体で人物を定義

(defstruct contact
  name
  email
  phone
  (tags nil))  ; タグのリスト

6-2. ハッシュテーブルで管理

;; 名前をキーにしたハッシュテーブル
(defparameter *contacts* (make-hash-table :test #'equal))

;; 連絡先を追加する関数
(defun add-contact (name email phone &optional tags)
  (setf (gethash name *contacts*)
        (make-contact :name name
                      :email email
                      :phone phone
                      :tags tags)))

;; 連絡先を検索する関数
(defun find-contact (name)
  (gethash name *contacts*))

;; 全連絡先を表示する関数
(defun list-contacts ()
  (maphash (lambda (name contact)
             (format t "~a: ~a~%"
                     name
                     (contact-email contact)))
           *contacts*))

6-3. 使用例

;; データを追加
(add-contact "Alice" "alice@example.com" "090-1234-5678" '(friend work))
(add-contact "Bob" "bob@example.com" "080-9876-5432" '(friend))

;; 検索
(defparameter *alice* (find-contact "Alice"))
(contact-email *alice*)
;; → "alice@example.com"

(contact-tags *alice*)
;; → (FRIEND WORK)

;; 一覧表示
(list-contacts)
;; Alice: alice@example.com
;; Bob: bob@example.com

6-4. タグで検索

;; 特定のタグを持つ連絡先を検索
(defun find-by-tag (tag)
  (let ((results nil))
    (maphash (lambda (name contact)
               (when (member tag (contact-tags contact))
                 (push contact results)))
             *contacts*)
    results))

(find-by-tag 'work)
;; → (#S(CONTACT :NAME "Alice" ...))

7. 練習課題

課題1:連想リストの操作

以下の連想リストから、指定されたキーの値を取得する関数 get-value を作れ。キーが存在しない場合はデフォルト値を返す。

(defparameter *config*
  '((host . "localhost")
    (port . 8080)
    (debug . t)))

(get-value 'host *config*)
;; → "localhost"

(get-value 'timeout *config* 30)
;; → 30(デフォルト値)

解答

(defun get-value (key alist &optional default)
  (let ((pair (assoc key alist)))
    (if pair
        (cdr pair)
        default)))

;; テスト
(get-value 'host *config*)
;; → "localhost"

(get-value 'timeout *config* 30)
;; → 30

課題2:プロパティリストの変換

連想リストをプロパティリストに変換する関数 alist-to-plist を作れ。

(alist-to-plist '((a . 1) (b . 2) (c . 3)))
;; → (A 1 B 2 C 3)

解答

(defun alist-to-plist (alist)
  (reduce #'append
          (mapcar (lambda (pair)
                    (list (car pair) (cdr pair)))
                  alist)))

;; テスト
(alist-to-plist '((a . 1) (b . 2) (c . 3)))
;; → (A 1 B 2 C 3)

課題3:ハッシュテーブルの統計

ハッシュテーブルの値(数値)の合計、平均、最大値を返す関数を作れ。

(defparameter *scores* (make-hash-table))
(setf (gethash 'alice *scores*) 85)
(setf (gethash 'bob *scores*) 92)
(setf (gethash 'carol *scores*) 78)

(hash-stats *scores*)
;; → (:SUM 255 :AVG 85.0 :MAX 92)

解答

(defun hash-stats (ht)
  (let ((values nil))
    (maphash (lambda (k v)
               (declare (ignore k))
               (push v values))
             ht)
    (if values
        (list :sum (reduce #'+ values)
              :avg (/ (reduce #'+ values)
                      (float (length values)))
              :max (reduce #'max values))
        (list :sum 0 :avg 0 :max 0))))

;; テスト
(hash-stats *scores*)
;; → (:SUM 255 :AVG 85.0 :MAX 92)

課題4:構造体の定義と操作

本を表す構造体 book を定義し、以下の操作を実装せよ。

  • フィールド:title, author, year, price
  • year のデフォルト値は 2024
  • 10%割引後の価格を返す関数 discounted-price

解答

(defstruct book
  title
  author
  (year 2024)
  price)

(defun discounted-price (book)
  (* (book-price book) 0.9))

;; テスト
(defparameter *my-book*
  (make-book :title "Common Lisp入門"
             :author "山田太郎"
             :price 3000))

(book-year *my-book*)
;; → 2024

(discounted-price *my-book*)
;; → 2700.0

課題5:図書館システム

構造体とハッシュテーブルを組み合わせて、簡単な図書館システムを作れ。

  • 本を登録する add-book
  • ISBN で検索する find-book
  • 著者名で検索する find-by-author

解答

(defstruct library-book
  isbn
  title
  author
  available)

(defparameter *library* (make-hash-table :test #'equal))

(defun add-book (isbn title author)
  (setf (gethash isbn *library*)
        (make-library-book :isbn isbn
                           :title title
                           :author author
                           :available t)))

(defun find-book (isbn)
  (gethash isbn *library*))

(defun find-by-author (author)
  (let ((results nil))
    (maphash (lambda (isbn book)
               (declare (ignore isbn))
               (when (string= author (library-book-author book))
                 (push book results)))
             *library*)
    results))

;; テスト
(add-book "978-1-234" "Lisp入門" "山田太郎")
(add-book "978-1-235" "Lisp応用" "山田太郎")
(add-book "978-1-236" "Python入門" "鈴木花子")

(library-book-title (find-book "978-1-234"))
;; → "Lisp入門"

(length (find-by-author "山田太郎"))
;; → 2

8. まとめ

データ構造の選び方

各データ構造の関数

データ構造 作成 取得 設定 削除
連想リスト acons, pairlis assoc acons -
プロパティリスト list getf setf + getf remf
ハッシュテーブル make-hash-table gethash setf + gethash remhash
構造体 make-xxx xxx-field setf + accessor -

使い分けの指針

  • 連想リスト:シンプル、小規模、イミュータブルなデータ
  • プロパティリスト:キーワード引数、シンボルの属性
  • ハッシュテーブル:大規模、高速検索が必要
  • 構造体:型安全、ドキュメント性、複雑なデータ
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?