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