Elisp Weak References

Today I added a skewer-eval-print-last-expression function to Skewer, functionality I’ve been sorely missing for awhile. To properly support it I needed a hash table with automatically expiring entries. Specifically, I needed to keep track of state in Emacs that I couldn’t trust the (untrusted) browser to track for me. The alternative would be to send an encrypted blob to the browser along with code to evaluate, which would send back with the result. Instead of getting into questionable, hand-rolled encryption I wrote an expiring hash table implementation.

This had me take a careful look over Elisp’s hash table documentation, which reminded me of a cool feature they have: key/value weakness. The hash table can be configured such that it doesn’t prevent its keys and values from being garbage collected. Elisp’s hash tables are really flexible in this regard; any combination of key and value weakness is supported. This is more flexible than Java’s WeakHashMap, which only supports weak keys. For example, to make a hash table that weakly holds its values,

(make-hash-table :weakness 'value)

Oddly, Elisp lacks functionality to use weak references more generally. Fortunately this can be fixed!

(defun weak-ref (thing)
  (let ((ref (make-hash-table :size 1 :weakness t :test 'eq)))
    (prog1 ref
      (puthash t thing ref))))

(defun deref (ref)
  (gethash t ref))

weak-ref wraps an object in a weak hash table of size 1 under the key t. The second function, deref, fetches the object from the hash table if it’s still there. Otherwise it returns nil. Here it is in action,

(setq ref (weak-ref (list 1 2 3)))

;; It's still there.
(deref ref)  ; => (1 2 3)

;; Now run garbage collection.

;; The list has been garbage collected.
(deref ref)  ; => nil

I had to use setq here instead of defvar because garbage collection seems to always get triggered after defvar.

I don’t have a use-case for this at the moment. Weak references are mostly useful in hash tables (caches), and these functions would be entirely redundant in that case. I originally implemented these as macros, but I feel it made them too inflexible — they couldn’t be passed as a function.

blog comments powered by Disqus

null program

Chris Wellons