Search code examples
common-lisphashtableequalityclos

Using CLOS class instances as hash-table keys?


I have the following class:

(defclass category ()
    ((cat-channel-name
    :accessor cat-channel-name :initarg :cat-channel-name :initform "" :type string
    :documentation "Name of the channel of this category")
    (cat-min
    :accessor cat-min :initarg :min :initform 0 :type number
    :documentation "Mininum value of category")
    (cat-max
    :accessor cat-max :initarg :max :initform 1 :type number
    :documentation "Maximum value of category"))
    (:documentation "A category"))

Now, I would like to use this class as a key for a hash-table. The addresses of instances can be easily compared with eq. The problem is however, there might be multiple identical instances of this category class and I would like the hash-table to recognize this as a key as well.

So, I was trying to overwrite the :test argument of the make-hash-table function like this:

(make-hash-table :test #'(lambda (a b) (and (equal (cat-channel-name a) (cat-channel-name b))
                                            (eq (cat-min a) (cat-min b))
                                            (eq (cat-max a) (cat-max b)))

Unfortunately, this is not allowed. :test needs to be a designator for one of the functions eq, eql, equal, or equalp.

One way to solve this would be to turn the class category into a struct, but I need it to be a class. Is there any way I can solve this?


Solution

  • You can use a more extensible hash table library, as explained in coredump's answer, but you could also use the approach that Common Lisp takes toward symbols: you can intern them. In this case, you just need an appropriate interning function that takes enough of a category to produce a canonical instance, and a hash table to store them. E.g., with a simplified category class:

    (defclass category ()
      ((name :accessor cat-name :initarg :name)
       (number :accessor cat-number :initarg :number)))
    
    (defparameter *categories*
      (make-hash-table :test 'equalp))
    
    (defun intern-category (name number)
      (let ((key (list name number)))
        (multiple-value-bind (category presentp)
            (gethash key *categories*)
          (if presentp category
              (setf (gethash key *categories*)
                    (make-instance 'category
                                   :name name
                                   :number number))))))
    

    Then, you can call intern-category with the same arguments and get the same object back, which you can safely use as a hash table key:

    (eq (intern-category "foo" 45)
        (intern-category "foo" 45))
    ;=> T