Using CLOS class instances as hash-table keys?
Asked Answered
C

3

7

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?

Cassy answered 20/11, 2015 at 13:52 Comment(2)
Why do you need it to be a class?Herries
Do you want to use instances as the keys, or the class itself?Redemption
R
6

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
Redemption answered 20/11, 2015 at 16:20 Comment(5)
I'll remember about that, nice approach.Herries
I think this is an elegant solution to my problem. Thanks!Cassy
This doesn't really answer the question. It just shows you can map slots to conses, which equalp hash tables can grok. But my best guess is that the op is aware of this, by mentioning the use of structs, which equalp hash tables can grok just as well.Containerize
@Paulo This doesn't really answer the question. it doesn't explain a way to get a hash table that hashes based on slot values,but it is an answer to "how can I use clos instances as hash table keys?" This answer is not the only one, but is "make sure that your equivalent instances are eq." And a way to do that is interning them. It's not the only solution, but in many cases it's useful.Redemption
@JoshuaTaylor, I agree, I didn't downvote it. But this is not generalizable given the question, so in my opinion, it should not be the accepted answer. And it leaks without weak hash tables.Containerize
H
7
  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=)
    
Herries answered 20/11, 2015 at 14:30 Comment(0)
C
7

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.

Cancel answered 20/11, 2015 at 17:34 Comment(0)
R
6

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
Redemption answered 20/11, 2015 at 16:20 Comment(5)
I'll remember about that, nice approach.Herries
I think this is an elegant solution to my problem. Thanks!Cassy
This doesn't really answer the question. It just shows you can map slots to conses, which equalp hash tables can grok. But my best guess is that the op is aware of this, by mentioning the use of structs, which equalp hash tables can grok just as well.Containerize
@Paulo This doesn't really answer the question. it doesn't explain a way to get a hash table that hashes based on slot values,but it is an answer to "how can I use clos instances as hash table keys?" This answer is not the only one, but is "make sure that your equivalent instances are eq." And a way to do that is interning them. It's not the only solution, but in many cases it's useful.Redemption
@JoshuaTaylor, I agree, I didn't downvote it. But this is not generalizable given the question, so in my opinion, it should not be the accepted answer. And it leaks without weak hash tables.Containerize

© 2022 - 2024 — McMap. All rights reserved.