lispcommon-lispsymbolsslotsclos

How do I access an unknown instance's slot using a string?


Problem

Given an instance, inst and a string attr containing the name of a slot, how can I obtain the value of the slot attr on inst?

Of course, if attr were a symbol rather than a string, I would typically just use (slot-value inst attr), but it seems I need the package information to properly call intern (see below).

Minimal example

(defpackage :pack1
  (:use :common-lisp)
  (:export :*inst*))

(in-package :pack1)

(defclass temp-class ()
  ((temp-slot :initarg :temp-slot)))

(defvar *inst* (make-instance 'temp-class :temp-slot "value"))

(defpackage :pack2
  (:use :common-lisp :pack1)
  (:import-from :pack1 :temp-class))

(in-package :pack2)

(let ((inst *inst*)  ; In the real example, inst gets defined outside my control,
                     ; in yet another package
      (attr "temp-slot"))
  (format t "Given package name: ~S; "  ; prints fine
          (slot-value inst (intern (string-upcase attr) :pack1)))
  (format t "No package name: ~S; "  ; signals an error
          (slot-value inst (intern (string-upcase attr)))))

Prior art

Background

I'm working on py-format a Common Lisp port of Python's {}-formatting. To implement the Python . operator (getattr) I need to convert the string following the dot into a slot on the object preceding the dot.


Solution

  • Given an instance, inst and a string attr containing the name of a slot, how can I obtain the value of the slot attr on inst?

    Slots don't have strings as slots names, but symbols. Since slot names can be arbitrary symbols, there is no general way to get a slot-value if all you have is a string.

    CL-USER 124 > (defclass foo ()
                    ((s)             ; the slot-name is cl-user::s
                     (system::s)     ; the slot-name is  system::s
                     (#:s)))         ; the slot-name is        #:s
    #<STANDARD-CLASS FOO 413054236B>
    

    The last slot-name is an uninterned symbol. It is in no package. Thus you can't look it up in any way, if you haven't stored it somewhere.

    CL-USER 125 > (make-instance 'foo)
    #<FOO 402013F043>
    
    CL-USER 126 > (describe *)
    
    #<FOO 402013F043> is a FOO
    S      #<unbound slot>
    S      #<unbound slot>
    S      #<unbound slot>
    

    As you see above, it has three slots. Each symbol has the name s, but is really a different symbol.

    You can get the slot names via introspection:

    CL-USER 127 > (mapcar #'slot-definition-name
                          (class-direct-slots (find-class 'foo)))
    (S SYSTEM::S #:S)
    

    For portable functions see CLOSER-MOP.