cl-hash-util
2024-10-12
A simple and natural wrapper around Common Lisp's hash functionality.
cl-hash-util
cl-hash-util is a very basic library for dealing with CL's hash tables. The idea was spawned through working with enough JSON APIs and config files, causing a lot of headaches in the process. For instance, to get a value deep within a hash, you have to do:
(gethash "city" (gethash "location" (gethash "user" obj)))
I find the inside-out approach unintuitive, regardless of how many lisp nerds are going to yell at me about how it's correct.
With cl-hash-util, you can write:
(hash-get obj '("user" "location" "city"))
hash-get can also deal with getting elements out of lists and arrays:
(hash-get obj '("user" "friends" 0 "name"))
which normally would have to be written as such:
(gethash "name" (elt (gethash "friends" (gethash "user" obj)) 0))
...uuuugly.
cl-hash-util also provides an easy way to build hash tables on the fly. Where you'd normally have to do something like:
(let ((myhash (make-hash-table :test #'equal))) (setf (gethash "name" myhash) "andrew") (setf (gethash "location" myhash) "santa cruz") myhash)
You can now do:
;; functional version (hash-create '(("name" "andrew") ("location" "santa cruz"))) ;; convenience macro `hash` (hash ("name" "andrew") ("location" "santa cruz"))
You can also do nested hashes:
(hash ("name" "andrew") ("location" (hash ("city" "santa cruz") ("state" "CA"))))
This saves a lot of typing =].
With-keys
With-keys is the hash table equivalent of with-slots.
(defvar ht (hash ("name" "andrew") ("location" "santa cruz"))) (with-keys ("name" (loc "location") (time "time" 2024)) ht (setf loc (string-upcase loc)) (format nil "Hi, ~a in ~a around ~a!" name loc time)) ;; => "Hi, andrew in SANTA CRUZ around 2024!"
The first parameter is a list of keys that with-keys will reference in the hash table provided in the second parameter. With-keys will attempt to convert each key into a symbol, binding the hash table value to it during body execution. String keys are upcased before conversion to symbols.
If you don't want with-keys to guess at a symbol for a key, supply a list -
(
If you want to supply a default value, you have to supply a list -
(
Collecting-hash-table
A collection macro that builds and outputs a hash table. To add to the hash table, call the collect function with a key and a value from within the scope of the collecting-hash-table macro. The value will be inserted or combined with existing values according to the specified accumulation mode.
This code collects words into bins based on their length:
(collecting-hash-table (:mode :append) (dotimes (i 10) (let ((word (format nil "~r" i))) (collect (length word) word)))
Result: <hash table: 5 => ("three" "seven" "eight") 3 => ("one" "two" "six") 4 => ("zero" "four" "five" "nine")>
The mode can be set in the parameters section of collecting-hash-table with the :mode keyword. The :mode keyword can also be passed to individual collect calls.
Keyword parameters:
:test - Test function parameter passed to make-hash-table when creating a new hash table
:existing - Pass an existing hash table to the macro for modification. Using this option at the same time as :test will result in an error.
:mode - Set the default mode for the collect function.
Modes
:replace - Acts the same as (setf (gethash key ht) value), replacing any existing value with the new one.
:keep - Only inserts the value if the key did not previously exist.
:tally - Ignores the input value, instead adding 1 to the key value.
:sum - Adds the input value to the key value. Input should be numeric.
:append - Appends the value to the list that is presumed to be under the key. If the key doesn't yet exist, places the value in a new list.
:push - Like append, but sticks things on the other end.
:concatenate - Assumes that both new and existing values are lists, storing the concatenation of them under the key.
Obviously, not all modes are compatible with each other. Collecting-hash-table makes no attempt to save you from intermingling them.
Custom modes may be created by supplying a function instead of a mode descriptor. This function will be applied in a reduce-like fashion: when a value already exists under a key, the existing value and the new value will be passed to the supplied function. Its return value will be stored under the key.
If more flexibility is needed, then a list of two functions can be supplied. The first function should accept two parameters: first, the existing value; second the new value. It will be called when a key already exists. The second function should take one parameter. It is called when a key does not exist yet. In both cases the key value is set to the function return value.
Conversion functions
Included is a suite of functions for converting between hash tables, alists and plists: alist->plist, plist->alist, alist->hash, plist->hash, hash->alist, and hash->plist.
The alist->hash and plist->hash take the same :existing and :mode keywords that collecting-hash-table takes.
Merging hash-tables
The function hash-merge
returns a merged hash-table.
If a key occurs more than one time, the first key value is used
and subsequent key values are discarded.
For keeping the last value of a key, reverse the hash-table order.
(hash-merge (hash-create (list (list "first" 1) (list "second" 2))) (hash-create (list (list "first" 234234) (list "third" 235346)))) ;; Contents: ;; "first" 1 ;; "second" 2 ;; "third" 235346