Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 705
JNevens Avatar asked Nov 20 '15 13:11

JNevens


3 Answers

Many Common Lisp implementations provide extensions to the ANSI Common Lisp standard to support different test and hash functions (and a lot more).

CL-CUSTOM-HASH-TABLE is a compatibility layer.

like image 194
Rainer Joswig Avatar answered Nov 13 '22 03:11

Rainer Joswig


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
like image 26
Joshua Taylor Avatar answered Nov 13 '22 03:11

Joshua Taylor


  1. Don't compare numbers with eq, use eql or =. From eq (emphasis mine):

    Objects that appear the same when printed are not necessarily eq to each other. [...] An implementation is permitted to make "copies" of characters and numbers at any time. The effect is that Common Lisp makes no guarantee that eq is true even when both its arguments are "the same thing" if that thing is a character or number.

  2. You can use the genhash library. First, you define a new hash function (see also sxhash) and a test function for your type and you associate it with a test designator:

    (genhash:register-test-designator
      'category= 
      (lambda (category) <hashing>)
      (lambda (a b) 
        (and (equal ... ...)
             (= ... ...)
             (= ... ...))))
    

    Then, you can define a new table:

    (genhash:make-generic-hashtable :test 'category=)
    
like image 6
coredump Avatar answered Nov 13 '22 01:11

coredump