Iterating Hash Table Entries
Modern Ways to Iterate Over Hash Tables in Lisp
Section titled “Modern Ways to Iterate Over Hash Tables in Lisp”Hash tables are a fundamental data structure in Common Lisp for storing key-value pairs efficiently. A common task is to iterate over all entries to process them. Modern Lisp offers several idiomatic ways to do this, each with its own advantages.
Creating a Hash Table: The Importance of :test
Section titled “Creating a Hash Table: The Importance of :test”Before we iterate, let’s create a hash table. The most important argument to make-hash-table is :test, which specifies how keys are compared.
#'eq: Compares objects by identity (memory location). Fast, but only works if keys are the exact same object. Good for symbols.#'eql(the default): Likeeq, but also works for identical characters and numbers.#'equal: Compares by structure. Use this for string keys or list keys.#'equalp: Likeequal, but case-insensitive for strings and characters.
;; A hash table for string keys MUST use #'equal or #'equalp(defvar *fruit-prices* (make-hash-table :test #'equal))
;; Populate the hash table using SETF with GETHASH(setf (gethash "apple" *fruit-prices*) 1.25)(setf (gethash "banana" *fruit-prices*) 0.75)(setf (gethash "orange" *fruit-prices*) 1.50)Method 1: The loop Macro (Recommended)
Section titled “Method 1: The loop Macro (Recommended)”The loop macro is the most flexible, readable, and idiomatic way to iterate over hash tables in modern Common Lisp. It provides dedicated clauses for this purpose.
(loop for key being the hash-key of <hash-table> using (hash-value value) do (your-code-here))Example
Section titled “Example”(loop for fruit being the hash-key of *fruit-prices* using (hash-value price) do (format t "The price of ~a is $~,2F~%" fruit price))Output (Order is Not Guaranteed)
Section titled “Output (Order is Not Guaranteed)”The price of orange is $1.50The price of apple is $1.25The price of banana is $0.75Why loop is great: It’s declarative, clearly states the intent, and handles all the iteration machinery for you. You can also use other loop clauses like collect, sum, or when to perform complex operations concisely.
Method 2: maphash (Functional Style)
Section titled “Method 2: maphash (Functional Style)”maphash applies a function to every key-value pair in the hash table. It’s best used when you need to perform an action (a side effect) for each entry. It does not return a meaningful value.
(maphash function hash-table)Example
Section titled “Example”;; The function must accept two arguments: the key and the value.(maphash (lambda (fruit price) (format t "Key: ~a, Value: $~,2F~%" fruit price)) *fruit-prices*)Output (Order is Not Guaranteed)
Section titled “Output (Order is Not Guaranteed)”Key: orange, Value: $1.50Key: apple, Value: $1.25Key: banana, Value: $0.75Method 3: with-hash-table-iterator (Low-Level Control)
Section titled “Method 3: with-hash-table-iterator (Low-Level Control)”This macro provides a lower-level, more manual way to iterate. It gives you an iterator function that you call repeatedly to get the next entry. This is the mechanism that loop uses internally. You typically only need this for highly specialized iteration patterns.
(with-hash-table-iterator (iterator-name hash-table) (loop (multiple-value-bind (entry-present-p key value) (iterator-name) (if entry-present-p (format t "Key: ~a, Value: ~a~%" key value) (return))))) ; Stop when no more entriesWhich Method Should You Use?
Section titled “Which Method Should You Use?”- Use
loopfor almost everything. It is the clearest, most powerful, and most common method. - Use
maphashwhen you want a simple, functional-style application of a function for its side effects. - Use
with-hash-table-iteratoronly whenloopandmaphashdon’t fit your needs, for example, if you need to pass the iterator itself to another function.
Testing Your Code
Section titled “Testing Your Code”When writing functions that process hash tables, remember to test them. A modern Lisp testing framework like FiveAM can be used to ensure correctness.
;; A conceptual test using a testing framework(fiveam:test find-expensive-fruits (let ((prices (make-hash-table :test 'equal))) (setf (gethash "apple" prices) 1.0) (setf (gethash "mango" prices) 3.0) (fiveam:is (equal '("mango") (find-expensive-fruits prices :min-price 2.0)))))