Skip to content

LISP - Accessing Elements of Lists

In Lisp, the fundamental data structure is the list, which is built from cons cells. A cons cell is a pair containing two pointers: the car (the value) and the cdr (a pointer to the next cell). A proper list is a chain of these cells ending with nil (the empty list).

Understanding this structure is key to mastering list manipulation. Lisp provides several powerful and idiomatic ways to access list elements.

These are the primitive functions for deconstructing a cons cell.

  • car: Returns the first element of the list. (Think “Contents of the Address Register”).
  • cdr: Returns the rest of the list (i.e., the list without its first element). (Think “Contents of the Decrement Register”).

While the names are historical artifacts from the IBM 704 computer, it’s often helpful to think of them as first and rest.

;; main.lisp
(let ((my-list '(A B C D)))
(print (car my-list)) ; Prints the first element
(print (cdr my-list)) ; Prints the rest of the list
)
A
(B C D)

Lisp provides convenient shorthand functions for nested calls of car and cdr. The function names are composed of a’s (for car) and d’s (for cdr) between a c and an r.

  • (cadr list) is equivalent to (car (cdr list)) (the second element).
  • (caddr list) is equivalent to (car (cdr (cdr list))) (the third element).
  • (cdar list) is equivalent to (cdr (car list)) (the rest of the first element, if it’s a list).
;; main.lisp
(let ((my-list '(A B C D)))
(print (cadr my-list)) ; Second element
(terpri)
(print (caddr my-list)) ; Third element
)
(let ((nested-list '((A 1) (B 2))))
(print (caar nested-list)) ; (car (car '((A 1) (B 2)))) -> A
(terpri)
(print (cdar nested-list)) ; (cdr (car '((A 1) (B 2)))) -> (1)
)
B
C
A
(1)

When you need to access an element by its zero-based index, nth is the function to use. Note that for lists, this can be inefficient as it has to traverse the list from the beginning (O(n) complexity).

elt is a more general function that works on any type of sequence, including lists and vectors (arrays).

;; main.lisp
(let ((my-list '(A B C D E)))
;; Get the 3rd element (index 2)
(print (nth 2 my-list))
;; nth returns NIL if the index is out of bounds
(print (nth 10 my-list))
)
C
NIL

For cleaner and more readable code, the destructuring-bind macro is a modern and idiomatic way to bind variables to parts of a list. It’s like pattern matching for list structures.

;; main.lisp
(let ((my-list '(A B C D E)))
(destructuring-bind (first second &rest the-rest) my-list
(format t "First: ~a~%" first)
(format t "Second: ~a~%" second)
(format t "The Rest: ~a~%" the-rest)))
;; It's also great for nested lists
(let ((config '(:host "localhost" :port 8080)))
(destructuring-bind (&key host port) config
(format t "Connecting to ~a on port ~a~%" host port)))
First: A
Second: B
The Rest: (C D E)
Connecting to localhost on port 8080

Recursion is a natural way to process lists in Lisp. A typical recursive function has a base case (usually checking for an empty list with null or endp) and a recursive step that processes the car and calls itself on the cdr.

;; main.lisp
(defun print-list-recursively (lst)
"Prints each element of a list on a new line using recursion."
;; Base Case: If the list is empty, do nothing and return.
(when lst
;; Process the first element
(print (car lst))
;; Recursive Step: Call the function on the rest of the list
(print-list-recursively (cdr lst))))
(print-list-recursively '(a b c d e))
A
B
C
D
E

Performance Note: Be mindful that deep recursion can lead to a stack overflow. Modern Common Lisp compilers often perform Tail Call Optimization (TCO), which can turn certain recursive forms into efficient loops, but the function must be in “tail position” (the very last thing it does).