【Common Lisp】
ハッシュテーブルの基本の使い方
(make-hash-table)

  • ハッシュテーブルは make-hash-table で作り、gethash / setf gethash / remhash で読み書き・削除する。
  • デフォルトの :testeql で、文字列キーには使えない。文字列キーには :test #'equal が必要。
  • alist・plist との使い分けの目安はエントリ数と操作頻度。数十件以上で検索が多いならハッシュテーブルを選ぶ。
  • 全エントリの走査には maphashloop for k being each hash-key of htwith-hash-table-iterator の3通りがある。

関連記事

1. ハッシュテーブルの基本

make-hash-table でテーブルを作り、
setf gethash で書き込み、
gethash で読み取ります。

ハッシュテーブルの基本 make-hash-table テーブルを作成 setf gethash 書き込み gethash 読み取り (let ((memo (make-hash-table))) (setf (gethash :a memo) 100 (gethash :b memo) 200) (gethash :b memo)) ;=> 200 T 戻り値は「値」と「存在T/F」の2つ

たとえば、階段を 1 段か 2 段ずつ登るとき、n 段の登り方は何通りかを求める例では、再帰の結果をハッシュテーブル memo にキャッシュすることで、重複計算を避けます。

(let ((memo (make-hash-table)))
  (defun climb-stairs (n)
    (cond ((= n 0) 1)
          ((= n 1) 1)
          ((gethash n memo))
          (t (setf (gethash n memo)
                   (+ (climb-stairs (- n 1))
                      (climb-stairs (- n 2))))))))

(climb-stairs 10)   ;=> 89
(climb-stairs 30)   ;=> 1346269Code language: Lisp (lisp)

condでは、条件式(gethash n memo)が 非nilを返したら、そのままその値を返すことができます。
つまり、memo に n で書き込まれていないときは計算してmemoに記録しています。

let の中で defun を書くと、キャッシュをグローバルに公開せずに保持できます1

1.1. gethashで参照・書き込み

gethashでキーをして書き込みます。
再度 setf すると値は上書きされます。
複数エントリを一度に書き込むときは setf に複数の place-value ペアを渡せます。

(defparameter *memo* (make-hash-table))
(setf (gethash :a *memo*) 1
      (gethash :b *memo*) 2
      (gethash :c *memo*) 3)Code language: Lisp (lisp)

gethash は2つの値を返します。
1つ目が格納された値、2つ目がキーの存在を示す真偽値です。

; gethash でキーを引く
(gethash :b *index*)
;=> 2
;   T

(gethash :e *index*)
;=> NIL
;   NILCode language: Lisp (lisp)

キーが存在しなかったとき、1つ目は nil になります。
ただし、第3引数にデフォルト値を渡せば、キーが存在しなかったときだけそれが返ります2

(gethash 99 *index* -1)
;=> -1
;   NILCode language: Lisp (lisp)

1.2. hash-table-count でエントリ数を得る

(hash-table-count *freq*)
;=> 4Code language: Lisp (lisp)

length はリストやベクタ向けで、ハッシュテーブルには使えません3

1.3. clrhash で全消去する

clrhash はテーブルのすべてのエントリを削除し、空になったテーブルを返します4

たとえば、複数のテストケースを処理するとき、テーブルを毎回作り直す代わりに clrhash で使い回せます。

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

(defun solve (test-cases)
  (loop for tc in test-cases
        do (clrhash *dp*)          ; テストケースごとにリセット
           (format t "~a~%" (process tc *dp*))))Code language: Lisp (lisp)

これで基本の読み書きは完結します。
以降の節では詳細を掘り下げます。

用途関数・マクロ
作成make-hash-table
読み取り・書き込みgethashsetf gethash
削除・全消去remhashclrhash
走査maphash
loop … hash-key … hash-value
with-hash-table-iterator
情報取得hash-table-counthash-table-size
hash-table-test
hash-table-rehash-size
hash-table-rehash-threshold
型確認hash-table-p
変換loop で alist 化

2. :test キーワードと比較関数

ハッシュテーブルを作るとき、最もよく指定するキーワード引数が :test です。
これはキーの比較に使う関数を決めます。

:test キーワードと比較関数 比較関数 同一とみなす対象 典型的なキー eql(デフォルト) 数値・文字・シンボルの同値 :keyword, 整数 #’equal 構造が等しいもの “string”, (list) #’equalp 大文字小文字を区別しない “Header”=”header” NG (make-hash-table) (setf (gethash “a” ht) 1) (gethash “a” ht) ;=> NIL NIL ← 見つからない OK :test #’equal (setf (gethash “a” ht) 1) (gethash “a” ht) ;=> 1 T ← 正常

指定できる値は eqeqlequalequalp の4つです。
省略すると eql になります5

比較関数同じとみなす対象典型的なキー
eq同一オブジェクトシンボル
eql数値・文字・シンボルの同値シンボル、整数、文字
equal構造が等しいもの文字列、リスト
equalp大文字小文字を区別しない文字列なども含む文字列(大小無視)

2.1. 文字列キーには :test #’equal が必要

デフォルトの eql は、文字列の内容ではなくオブジェクトの同一性で比較します。

そのため、同じ内容の文字列リテラルでも別オブジェクトとして扱われ、キーとして機能しません6

標準入力から読んだ文字列をキーにする場合、:test #'equal がないとヒットしません。

; NG: デフォルト (eql) で文字列をキーにする
(defparameter *dict* (make-hash-table))
(setf (gethash "apple" *dict*) 1)
(gethash "apple" *dict*)
;=> NIL   ; 見つからない
;   NIL

; OK: :test #'equal を指定する
(defparameter *dict* (make-hash-table :test #'equal))
(setf (gethash "apple" *dict*) 1)
(gethash "apple" *dict*)
;=> 1
;   TCode language: Lisp (lisp)

#'equal のかわりに 'equal と書いても同じ結果になります7

2.2. キーワードシンボルと文字列の違い

シンボルを eql で比較する場合はこの問題が起きません。

:alice のようなキーワードシンボルはプログラム中で唯一のオブジェクトだからです8

文字列キーが必要ない場面ではキーワードシンボルを使うと :test の指定を省けます。

2.3. equalp で大文字小文字を区別しない

HTTP ヘッダーのように大文字小文字が混在するキーには equalp が便利です9

(defparameter *headers* (make-hash-table :test #'equalp))
(setf (gethash "Content-Type" *headers*) "application/json")

(gethash "content-type" *headers*)
;=> "application/json"
;   TCode language: Lisp (lisp)

2.4. 複合キー(座標・状態のペア)

2次元グリッドや BFS の訪問済み管理など、複数の値を組み合わせたキーが必要な場面に使います。
cons をキーにするには :test #'equal が必要です。

; BFS で迷路を解く際の訪問済みセット
(defun bfs-solve (start goal neighbors)
  (let ((visited (make-hash-table :test #'equal))
        (queue (list (list start))))
    (setf (gethash start visited) t)
    (loop while queue
          for (path . rest) = queue
          for node = (car (last path))
          do (setf queue rest)
             (when (equal node goal)
               (return path))
             (loop for next in (funcall neighbors node)
                   unless (gethash next visited)
                     do (setf (gethash next visited) t)
                        (setf queue (append queue (list (append path (list next)))))))))

; (row . col) を複合キーとして使う例
(defun make-grid-visited ()
  (make-hash-table :test #'equal))

(defun mark-visited (visited row col)
  (setf (gethash (cons row col) visited) t))

(defun visitedp-grid (visited row col)
  (gethash (cons row col) visited))Code language: Lisp (lisp)

equal でコンスセルを複合キーにする場合、毎アクセスで再帰的な構造比較が走ります10
2次元配列のサイズが決まっているなら (+ (* row cols) col) のような整数キーに変換する方が速くなります。

3. ハッシュテーブルを走査する

ハッシュテーブルのエントリに対して繰り返し処理をする方法は3通りあります。
いずれの方法でも、エントリの取り出し順序は保証されません11

ハッシュテーブルを走査する 取り出し順序は保証されない maphash 全エントリに関数を適用 戻り値は常に nil (maphash (lambda (k v) (print k v)) ht) 副作用向き loop collect / sum と組み合わせ やすい loop for k being each hash-key of ht using (hash-value v) 収集・集計向き with-hash-table -iterator 途中で打ち切り可能 低レベルなAPI (next) で 3値を返す: more-p key value 早期終了向き

3.1. maphash で走査する

maphash はキーと値を引数に取る関数をすべてのエントリに適用します。
戻り値は常に nil です。

単語の出現頻度を集計した後、閾値以上の単語だけ出力する例です。

(defun print-frequent-words (freq min-count)
  (maphash (lambda (word count)
             (when (>= count min-count)
               (format t "~a: ~a~%" word count)))
           freq))

(defparameter *freq* (make-hash-table :test #'equal))
(setf (gethash "the"  *freq*) 10
      (gethash "cat"  *freq*)  3
      (gethash "sat"  *freq*)  1
      (gethash "mat"  *freq*)  2)

(print-frequent-words *freq* 3)
; the: 10
; cat: 3Code language: Lisp (lisp)

走査中に remhash でエントリを削除することは許されています。
ただし、走査中に新しいエントリを追加した場合、そのエントリが走査されるかどうかは未定義です12

3.2. loop で走査する

loophash-key 節と hash-value 節を使うと、キーと値を同時に取り出せます。
maphash より collectsum などと組み合わせやすい点が利点です13

頻度テーブルの最大値を持つキーを求める例です。

(defun most-frequent (freq)
  (loop for word being each hash-key of freq
        using (hash-value count)
        for best = (cons word count) then (if (> count (cdr best))
                                              (cons word count)
                                              best)
        finally (return (car best))))

(most-frequent *freq*)
;=> "the"Code language: Lisp (lisp)

キーと値を同時に alist に変換したい場合も loop が使いやすいです。

(defun freq->sorted-alist (freq)
  (sort (loop for w being each hash-key of freq
              using (hash-value c)
              collect (cons w c))
        #'> :key #'cdr))

(freq->sorted-alist *freq*)
;=> (("the" . 10) ("cat" . 3) ("mat" . 2) ("sat" . 1))Code language: Lisp (lisp)

3.3. ハッシュテーブルのキーと値をリストにする

標準だけで書くなら loop が簡単です。
Alexandria ライブラリを使えば hash-table-keyshash-table-values が使えます14

(defun hash-table-keys (table)
  (loop for k being each hash-key of table collect k))

(defun hash-table-values (table)
  (loop for v being each hash-value of table collect v))

; 頻度テーブルから上位 3 語を取り出す
(defun top-n (freq n)
  (subseq (freq->sorted-alist freq) 0 n))Code language: Lisp (lisp)

3.4. 頻度カウント(文字・単語の出現数)

incfgethash のデフォルト値を組み合わせるのが慣用句です。
文字列中の各文字の出現回数を数える例です。

(defun char-frequency (s)
  (let ((freq (make-hash-table)))
    (loop for c across s
          do (incf (gethash c freq 0)))
    freq))

(defun freq->sorted (freq)
  (sort (loop for k being each hash-key of freq
              using (hash-value v)
              collect (cons k v))
        #'> :key #'cdr))

(freq->sorted (char-frequency "abracadabra"))
;=> ((#\a . 5) (#\b . 2) (#\r . 2) (#\c . 1) (#\d . 1))Code language: Lisp (lisp)

incf(setf (gethash c freq) (1+ (gethash c freq 0))) の短縮形です15

3.5. with-hash-table-iterator で走査する

with-hash-table-iterator はイテレータを使った低レベルな走査方法です。
maphashloop で対応できない場合の手段として存在します16

条件を満たす最初のエントリが見つかった時点で走査を打ち切る例です。

(defun find-first-above (freq threshold)
  (with-hash-table-iterator (next freq)
    (loop
      (multiple-value-bind (more-p word count)
          (next)
        (unless more-p (return nil))
        (when (> count threshold)
          (return (cons word count)))))))

(find-first-above *freq* 5)
;=> ("the" . 10)   ; 見つかった時点で終了Code language: Lisp (lisp)

next を呼ぶたびに3つの値が返ります。1つ目がエントリの有無(nil になったら終了)、2つ目がキー、3つ目が値です。

実用上は maphashloop で書けることがほとんどです。

4. ハッシュ関数と再ハッシュの仕組み

ハッシュテーブルは内部に配列を持ち、キーのハッシュ値をインデックスとして値を格納します。

そのため、nth のようにリストをたどる必要がなく、エントリ数によらず O(1) でアクセスできます。

ただし、複数のキーが同じインデックスに衝突する「コリジョン」が起きると、線形探索や連鎖リストで解決するため最悪 O(N) になります。
良いハッシュ関数と適切なテーブルサイズを保てばコリジョンは稀なので、平均 O(1) として使えます17

4.1. テーブルが満杯になると再ハッシュが起きる

エントリ数が内部容量の一定割合(rehash-threshold)を超えると、テーブルが自動的に拡張されます。
このとき全エントリのハッシュ値を再計算して新しい配列に移し直す「再ハッシュ」が発生し、一時的にコストがかかります。

大量のエントリを事前に入れるとわかっているなら、最初から :size で余裕を持った容量を指定しておくと再ハッシュの回数を減らせます18

; N 件のデータを扱う前提でテーブルを作る
(defun make-table-for (n)
  (make-hash-table :test #'equal :size (round (* n 1.5))))Code language: Lisp (lisp)

4.2. rehash-size で拡張量を制御する

拡張のたびにどれだけ大きくするかは :rehash-size で指定します。

整数を渡すと加算、浮動小数を渡すと現在サイズへの乗算になります19

; 容量が足りなくなったら 1000 件分追加する
(make-hash-table :size 100 :rehash-size 1000)

; 容量が足りなくなったら 1.5 倍に拡張する
(make-hash-table :size 100 :rehash-size 1.5)Code language: Lisp (lisp)

エントリ数が段階的に増える場合は加算、急激に増える場合は乗算が効率的です。
なお、実装はこれらのヒントを無視してもよい、と仕様で定められています。

5. alist・plist との使い分け

Common Lisp には連想データを扱う方法が3つあります。
それぞれに得意な場面があります。

alist・plist との使い分け 構造 検索 エントリ数目安 特徴 alist O(N) 数十件以下 非破壊的に拡張可 plist O(N) 数件程度 引数の受け渡しに自然 hash-table O(1) 平均 数十件以上 破壊的操作・更新向き 件数が少ない alist / plist 件数が増えたら make-hash-table 検索・更新が増えたタイミングで切り替える
構造検索追加エントリ数の目安備考
alistO(N)O(1)少ない(数十件以下)assoc で先頭から線形探索
plistO(N)O(1)少ない(数件程度)キーワード引数の受け渡しに自然
hash tableO(1) 平均O(1) 平均多い(数十件以上)可変・破壊的操作が前提

alist は非破壊的に拡張できます。acons で先頭に追加した新しいリストを返せるため、関数型スタイルで履歴を保持しながら更新する用途に向いています20
ハッシュテーブルは常に破壊的な操作になるため、immutable なデータとして扱うことはできません。

エントリ数が少なくて読み取り専用に近いなら alist、多くなって検索・更新が頻繁なら make-hash-table に切り替えるのが自然な流れです。

5.1. alist をハッシュテーブルに変換する

既存の alist をハッシュテーブルに載せ替えるには loop で回すのが簡単です21

(defun alist->hash-table (alist &key (test #'eql))
  (let ((table (make-hash-table :test test)))
    (loop for (key . value) in alist
          do (setf (gethash key table) value))
    table))

(defparameter *price-list*
  '(("apple" . 120) ("banana" . 80) ("cherry" . 300)))

(defparameter *prices* (alist->hash-table *price-list* :test #'equal))

(gethash "banana" *prices*)
;=> 80
;   TCode language: Lisp (lisp)

5.2. ハッシュテーブルを alist に変換する

loophash-key 節で逆方向の変換もできます22

(defun hash-table->alist (table)
  (loop for k being each hash-key of table
        using (hash-value v)
        collect (cons k v)))

(hash-table->alist *prices*)
;=> (("cherry" . 300) ("banana" . 80) ("apple" . 120))Code language: Lisp (lisp)

6. 存在確認と第2戻り値

6.1. nil を値として格納するときの落とし穴

gethash の第1戻り値だけで存在確認をすると、値が nil のエントリで誤判定します。

訪問済みフラグのように「キーが存在すること自体」に意味がある場合は特に注意が必要です。

(defparameter *visited* (make-hash-table))
(setf (gethash 0 *visited*) nil)   ; ノード 0 を訪問済みとしてマーク

; 第1戻り値だけ見ると「未訪問」に見える
(if (gethash 0 *visited*)
    "訪問済み"
    "未訪問")
;=> "未訪問"   ; 間違いCode language: Lisp (lisp)

正確に確認するには第2戻り値を使います。

(defun visitedp (table node)
  (nth-value 1 (gethash node table)))

(visitedp *visited* 0)
;=> T      ; 正しく訪問済みと判定できる

(visitedp *visited* 5)
;=> NIL    ; 未訪問Code language: Lisp (lisp)

nth-value は多値の n 番目(0始まり)だけを取り出す関数です23

multiple-value-bind で両方を受け取ることもできます。
ハッシュテーブルから値を取り出しつつ、見つからなければ別の処理を走らせたい場合に使います。

; キャッシュから取得し、なければ計算して格納する
(defun cached-square (table n)
  (multiple-value-bind (val found-p)
      (gethash n table)
    (if found-p
        val
        (setf (gethash n table) (* n n)))))

(defparameter *sq* (make-hash-table))
(cached-square *sq* 7)   ;=> 49   (計算して格納)
(cached-square *sq* 7)   ;=> 49   (キャッシュから返る)Code language: Lisp (lisp)

6.2. Two Sum(ハッシュテーブルによる O(N) 解法)

「リストの中から和が target になる2つの要素のインデックスを返す」問題です。

各要素を走査しながら「target – 現在値」がすでに登録されているか調べます。

(defun two-sum (nums target)
  (let ((seen (make-hash-table)))
    (loop for n in nums
          for i from 0
          for complement = (- target n)
          do (multiple-value-bind (j found-p)
                 (gethash complement seen)
               (when found-p
                 (return (list j i)))
               (setf (gethash n seen) i)))))

(two-sum '(2 7 11 15) 9)    ;=> (0 1)
(two-sum '(3 2 4)    6)    ;=> (1 2)Code language: Lisp (lisp)

7. エントリの削除

7.1. remhash でエントリを削除する

remhash はキーとテーブルを受け取り、エントリを削除します。
キーが存在したかどうかを戻り値として返します24

スライディングウィンドウで「ウィンドウ外に出た要素をテーブルから取り除く」場面が典型的な用途です。

; 長さ k のウィンドウ内に重複があるか調べる
(defun contains-nearby-duplicate-p (nums k)
  (let ((table (make-hash-table)))
    (loop for i from 0
          for n in nums
          do (when (nth-value 1 (gethash n table))
               (return-from contains-nearby-duplicate-p t))
             (setf (gethash n table) i)
             (when (> i (1- k))
               (remhash (nth (- i k) nums) table)))
    nil))

(contains-nearby-duplicate-p '(1 2 3 1) 3)   ;=> T
(contains-nearby-duplicate-p '(1 2 3 4) 3)   ;=> NILCode language: Lisp (lisp)

8. テーブルの情報を取得する

8.1. hash-table-p で型を確認する

(hash-table-p *freq*)
;=> T

(hash-table-p '(("a" . 1) ("b" . 2)))
;=> NILCode language: Lisp (lisp)

8.2. hash-table-test で比較関数を確認する

(hash-table-test *freq*)
;=> EQUAL

(hash-table-test (make-hash-table))
;=> EQLCode language: Lisp (lisp)

8.3. hash-table-size で確保済み容量を知る

hash-table-count がエントリ数なのに対し、hash-table-size は内部的に確保されている容量を返します25

(defparameter *small* (make-hash-table :size 4))
(hash-table-size *small*)
;=> 7   ; 実装が次の「よい」サイズに丸める場合があるCode language: Lisp (lisp)

:size は確保する初期容量のヒントです。
実際の容量は実装が丸めるため、指定値と一致しない場合があります。

9. 実践パターン

9.1. グループ化(同じキーでまとめる)

アナグラム(同じ文字の並び替え)をグループ化する例です。

各単語をソートした文字列をキーにすると、同じアナグラム同士が同じバケットに入ります。

(defun group-anagrams (words)
  (let ((groups (make-hash-table :test #'equal)))
    (loop for w in words
          for key = (coerce (sort (coerce w 'list) #'char<) 'string)
          do (push w (gethash key groups '())))
    (loop for v being each hash-value of groups
          collect (nreverse v))))

(group-anagrams '("eat" "tea" "tan" "ate" "nat" "bat"))
;=> (("eat" "tea" "ate") ("tan" "nat") ("bat"))Code language: Lisp (lisp)

push でリストを積み上げているので、最後に nreverse をかけて元の順序に戻します26

  1. let の中で defun を書くと、defun で定義される関数名はグローバルな名前空間に登録されますが、let で束縛された変数はクロージャ変数になります。関数自体もローカルに閉じ込めたい場合は fletlabels を使います。 – The Common Lisp Cookbook – Functions
  2. setf と組み合わせるとき、(setf (gethash key table default) value) のように第3引数を書いても、その値は評価されますが無視されます。HyperSpec にそのように定義されています。 – Accessor GETHASH – Common Lisp HyperSpec
  3. hash-table-count はハッシュテーブル専用の関数です。length はシーケンス(リスト・ベクタ・文字列)にしか使えないため、ハッシュテーブルに対して呼ぶとエラーになります。エントリ数の確認には必ず hash-table-count を使います。 – The Common Lisp Cookbook – Hash Tables
  4. clrhash はテーブルオブジェクト自体を返します。テスト間でテーブルを使い回す場合、clrhash の戻り値はテーブル自体なので (defparameter *t* (clrhash *t*)) のような再代入は不要です。 – Hash Table Functions – Common Lisp the Language 2nd ed.
  5. HyperSpec では :test に指定できる値を eqeqlequalequalp の4つに限定しています。SBCL など一部の実装は独自拡張として任意の関数を :test に渡したり、:hash-function キーワードで独自のハッシュ関数を指定できるものもありますが、これらはポータブルではありません。 – Function MAKE-HASH-TABLE – Common Lisp HyperSpec
  6. eql は文字列の内容を比較しません。CLtL2 では「eql は数値と文字の値を比較するが、文字列の内容は比較しない」と明記されています。文字列の内容を比較するには equalequalpstring=string-equal などを使う必要があります。 – Equality Predicates – Common Lisp the Language 2nd ed.
  7. HyperSpec の make-hash-table の定義では :test にシンボル('equal)と関数オブジェクト(#'equal)のどちらを渡しても同じ動作になると定義されています。実際の内部表現はシンボルで保持され、hash-table-test の戻り値は常にシンボルとなります。 – Function MAKE-HASH-TABLE – Common Lisp HyperSpec
  8. シンボルはリーダーが読み込み時にパッケージのシンボルテーブルへ「インターン」します。同じ名前のシンボルは同一のオブジェクトとして再利用されるため、(eq :alice :alice) は常に T になります。キーワードシンボルは KEYWORD パッケージに登録され、外部シンボルとして公開されます。 – Packages and Symbols – Practical Common Lisp
  9. equalp は文字列の大文字小文字を区別しないほか、数値型の違いも無視します(例:(equalp 1 1.0)T)。ハッシュテーブルのキーとして数値を使う場合、eql では 11.0 が別キーとして扱われますが、equalp では同一キーになります。 – Equality Predicates – Common Lisp the Language 2nd ed.
  10. equal でリストやコンスセルを複合キーにする場合、毎回のアクセスで equal による再帰的な構造比較が走ります。高頻度でアクセスするなら、複合キーを文字列や整数に変換してからキーにする方法(例:(format nil "~a,~a" row col) や行列インデックスの線形化)の方が効率的な場合があります。 – Hash Tables in Common Lisp – Exercism
  11. ハッシュテーブルの走査順序は仕様で未定義です。同じテーブルを異なる実装や異なるタイミングで走査しても、同じ順序になる保証はありません。順序に依存するコードは書かないようにする必要があります。 – The Common Lisp Cookbook – Hash Tables
  12. HyperSpec では、maphash の走査中に許可されるのは「現在処理中のエントリの値を setf gethash で変更すること」と「現在処理中のエントリを remhash で削除すること」の2つだけです。これは X3J13 の MAPPING-DESTRUCTIVE-INTERACTION 決議で定められました。 – Function MAPHASH – Common Lisp HyperSpec
  13. loophash-key および hash-value 節は CLtL2 と HyperSpec の両方で定義されています。for k being each hash-key of htfor k being the hash-keys of ht はどちらも同じ意味で使えます。 – The Common Lisp Cookbook – Hash Tables
  14. Alexandria は Quicklisp で配布されている Common Lisp の準標準ユーティリティライブラリです。(ql:quickload "alexandria") でロードした後、(alexandria:hash-table-keys *ht*) のように使えます。 – The Common Lisp Cookbook – Data structures
  15. HyperSpec の gethash の使用例にも incfgethash のデフォルト値を組み合わせた頻度カウントが掲載されています。(incf (gethash item table 0)) は、キーが未登録の場合は 0 をデフォルトとして読み取り、インクリメントした値を書き込む慣用句です。 – Accessor GETHASH – Common Lisp HyperSpec
  16. CLtL2 では with-hash-table-iterator の設計意図として「maphash よりも柔軟で、loop のハッシュテーブル走査節をポータブルかつ効率的に実装するために使える」と説明されています。また、イテレータを with-hash-table-iterator のダイナミックエクステント外に持ち出した場合の動作は未定義と明記されています。 – Hash Table Functions – Common Lisp the Language 2nd ed.
  17. Common Lisp ではキーのハッシュ値を計算する低レベル関数として sxhash が提供されています。sxhash の計算方法は実装依存ですが、equal で等しいオブジェクトは同じハッシュ値を返すことが仕様で保証されています。また同一セッション内でオブジェクトが equal の意味で変更されない限り、ハッシュ値は変わりません。 – Function SXHASH – Common Lisp HyperSpec
  18. CL Cookbook の計測例によると、10万件のデータを :rehash-size 100000 付きで作成した場合は再サイズが1回で済み、デフォルト設定より大量のコンシングを避けられます。ただし実装はこれらのヒントを無視してもよいと仕様で定められています。 – The Common Lisp Cookbook – Hash Tables
  19. HyperSpec では :rehash-size に整数を渡すと「加算的成長(additive growth)」、浮動小数を渡すと「乗算的成長(multiplicative growth)」と定義しています。乗算では新旧サイズの比率が指定値になります。なお 1.0 より小さい浮動小数を渡すと縮小になり、動作が実装依存になる場合があります。 – Function MAKE-HASH-TABLE – Common Lisp HyperSpec
  20. CLtL2 では「alist は非破壊的に拡張できる(acons で先頭に追加した新しいリストを返す)のに対し、ハッシュテーブルへの追加は常に破壊的操作になる」と明記されています。immutable なデータ構造として扱いたい場合は alist が適切です。 – Hash Tables – Common Lisp the Language 2nd ed.
  21. Alexandria ライブラリには alexandria:alist-hash-table という専用関数があります。:test キーワードでテストを指定でき、重複キーの扱いも制御できます。標準のみに限定するなら loop を使った実装が最もポータブルです。 – The Common Lisp Cookbook – Data structures
  22. Alexandria ライブラリには alexandria:hash-table-alist および alexandria:hash-table-keysalexandria:hash-table-values が用意されています。標準だけで書く場合は loop を使います。 – The Common Lisp Cookbook – Data structures
  23. 多値の典型的なユースケースとして HyperSpec と Practical Common Lisp のどちらも gethash を挙げています。「第1戻り値が nil でも、それがキーの不在を意味するとは限らない。第2戻り値で区別できる」という設計は Common Lisp における多値の代表的な利用例です。 – Collections – Practical Common Lisp
  24. remhash を同じキーで2回呼んでも安全です。2回目は単に nil を返します。また、maphash の走査中に現在処理しているエントリを remhash で削除することは仕様上許可されています。 – Function REMHASH – Common Lisp Language Reference
  25. hash-table-size の戻り値は実装依存であり、:size キーワードで指定した値と一致するとは限りません。実装は「よいサイズ」(例えば次の素数)に切り上げることが許されています。HyperSpec にも「:size は実装へのヒントであり、実際の初期サイズは異なる場合がある」と定義されています。 – Function MAKE-HASH-TABLE – Common Lisp HyperSpec
  26. push(setf var (cons item var)) の短縮形です。(gethash key groups '()) のようにデフォルト値を指定するとキー未登録時でも nil(空リスト)が返り、cons で先頭に追加できます。ただし push はマクロで、(gethash ...) のような複合形式を place として使う場合は (setf (gethash ...) (cons item (gethash ...))) に展開されます。 – The Common Lisp Cookbook – Data structures