A common frustration for (impatient) beginners is to see different function names to access common data structures (alists, plists, hash-tables) and their inconsistencies (the order of arguments).
Now they are well documented in the… Common Lisp Coobook of course: https://lispcookbook.github.io/cl-cookbook/data-structures.html, but still;
and it is annoying to try things out with a data structure and refactor the code to use another one.
The library Access solves those problems, it’s always
(access my-var elt)
(if you’re into this, note that CL21 also does this with a generic and extensible getf
).
edit: also rutils with generic-elt
or ?
in the rutilsx
contrib package.
Access also solves another usecase.
Sometimes we deal with nested data structures (alist inside alist
inside alist, or mixed data structures, happens when working with an
API) and, as in other languages, we’d like a shortcut to access a
nested element. In Python, we can use addict
to write
foo.one.2.three
instead of foo['one'][2]['three']
, with Access we
have two possibilities, see below.
Oh, and we can be confident it is a battle-tested library, since it is the one that powers Djula’s template variables interplolation (doc is here), where we can write
{{ var.foo }}
à la Django for the supported data structures, and Djula is in the top 100 of the most downloaded Quicklisp libraries (december 2017 stats).
Let’s install it:
(ql:quickload "access")
import its symbols in Slime:
(use-package :access)
Generic and consistent access accross alists, plists, hash-tables, CLOS slots
Let’s create our test variables first:
(defparameter my-alist '((:foo . "foo") (:bar . "bar")))
MY-ALIST
(defparameter my-plist (list :foo "foo" :bar "bar"))
MY-PLIST
(defparameter my-hashtable (make-hash-table))
MY-HASHTABLE
(setf (gethash :foo my-hashtable) "foo")
"foo"
(defclass obj-test ()
((foo :accessor foo :initarg :foo :initform :foo)
(bar :accessor bar :initarg :bar :initform :bar)))
;; #<STANDARD-CLASS OBJ-TEST>
(defparameter my-obj (make-instance 'obj-test))
;; MY-OBJ
Now, let’s access the :foo
slot.
With alists:
(access my-alist :foo)
"foo"
T
instead of (cdr (assoc :foo my-alist))
(with :foo first argument) or alexandria’s (assoc-value my-alist :foo)
(:foo second argument).
plists:
(access my-plist :foo)
"foo"
T
instead of (getf my-plist :foo)
(unlike alists, with :foo as last argument).
hash-tables:
(access my-hashtable :foo)
"foo"
T
instead of (gethash :foo my-hashtable)
(:foo first argument).
objects:
(access my-obj :foo) ;; <= accessor, not slot name
;; :FOO
;; T
instead of… it depends. Here we named the accessor foo
, so we would have used simply (foo my-obj)
.
Also note that access
returns two values, the value and a boolean, t
if the slot exists, nil otherwise.
And access
is setf
able:
(setf (access my-alist :foo) "oof")
with-access
Below, we can bind temporary variables inside with-access
:
(with-access (foo bar (other-name plist))
my-obj
(format t "Got: ~a~a~a~&" foo bar other-name)
;; we can change variables
(setf other-name "hello plist")
(format t "other-name: ~a~&" other-name)
;; it changed the object too
(format t "object slot: ~a~&" (plist my-obj)))
Got: FOOBAR(FOO foo BAR bar)
other-name: hello plist
object slot: hello plist
NIL
Nested access
For this example we add a plist
slot to our object, which copies our my-plist
by default.
(defclass obj-test ()
((foo :accessor foo :initarg :foo :initform :foo)
(bar :accessor bar :initarg :bar :initform :bar)
(plist :accessor plist :initarg plist :initform (copy-list MY-PLIST))))
#<STANDARD-CLASS OBJ-TEST>
(as being a CLOS object, my-obj
is automatically updated with the new slot).
We can access the nested plist element :foo
inside the object in one go with accesses
(plurial):
(accesses MY-OBJ 'plist :foo)
;; "foo"
instead of (getf (plist my-obj) :foo)
.
Dotted access
with-dot
or #D
We can rewrite the previous examples with a dot:
(with-dot ()
my-alist.foo)
"foo"
or again
(with-dot ()
my-obj.foo)
"hello plist"
but even shorter, with the #D
reader macro that we enable with
(enable-dot-syntax)
(also works in Slime/Sly, for what I am not sure if I enabled a special feature)
#Dmy-alist.foo
"foo"
and so, a nested dotted access through an object and a plist:
;; back to initial case
(setf my-obj (make-instance 'obj-test))
;; #<OBJ-TEST {1005AA3B13}>
#Dmy-obj.plist.foo
;; "foo"
It will return nil
instead of an error if someone in the middle
doesn’t have the requested field.
Usage will tell how it is useful, and I hope it will be to fellow newcomers.