BEYOND LISTS
Lists are the most important data structure in Lisp, but Common Lisp includes the very best object system for doing object-oriented programming, hash-tables, arrays, and structures. Lists can do everything they do, but for speed and efficiency purposes, you'll want to use these at some point.
OOP
OOP is done through the CLOS–the Common Lisp Object System.
In CLOS, classes are user-defined data types that group related data. Unlike
the OOP systems found elsewhere, Common Lisp classes don't package data and an
API for acting on that data together in the same module. Instead, classes define
types of data that can be targeted or specialized for by typecase forms and
generic functions/methods. The "verbs" for an object live separately.
Class Basics
(defclass creature ()
((name :initarg :name
:initform nil
:accessor creature-name)
(territory :initarg :territory
:initform "Earth"
:accessor creature-territory)))
This is the basic structure of a class definition. defclass takes at least
three arguments: a name, a list of classes to inherit from, and slot
definitions.
Use make-instance to make an instance of the class.
(make-instance 'creature :name "Cat")
Slot definitions can take a number of options.
:initargs defines the keyword argument used for setting the slot's value when creating an
instance of the class. In the example below, :name is the :initarg.
(defparameter *cat* (make-instance 'creature :name "Cat"))
:initform defines the default value of the slot if none is provided at the time of
instantiation.
(creature-territory *cat*)
Result"Earth"
When a class inherits from another class or classes, its definition includes
everything in the superclass(es) plus the additional slots it's own definition
includes. You can also overwrite the definition of a superclass's slot.
(defclass person (creature)
((name :initarg :creature-name
:initform "John Doe"
:accessor person-name)
(marrital-status :initarg :marrital-status
:initform :single
:accessor marrital-status)))
(defparameter *john-doe* (make-instance 'person :marrital-status :married))
The name slot falls back to its :initform:
(person-name *john-doe*)
Result"John Doe"
:accessor defines the generic method that can be used to both access and modify the
slot's value.
(creature-name *cat*)
Result"Cat"
(setf (creature-name *cat*) "Tiger")
(creature-name *cat*)
Result"Tiger"
Because accessors are generic methods specialized on instances of the class that defines them, two classes can define accessors with the same name without any conflicts.
In practice, you'll often see accessors that follow a <class-name>-<slot-name>
convention for clarity.
Usually you want an accessor–a method that allows you to both read and write to
a slot. In some cases, as in condition definitions, you may want to only allow
the program to read from a slot. In that case, you can replace :accessor with
:reader. Of course, you can define a writer with :writer.
Custom Constructors
While you can manually called make-instance all the time, it's common to
define a custom constructor for objects.
(defun make-creature (name territory)
(make-instance 'creature :name name :territory territory))
In addition to reducing typing in the simple case, custom constructors are also
where you can filter input, add type-checking with etypecase, modify data
before making an instance, etc.
Generic Functions & Methods
Classes describe what an object has, but they don't say anything about what an
object does. That's because in Common Lisp, classes don't have methods;
generic functions have methods.
(defclass person ()
((name :initarg :name :initform nil :accessor person-name)))
(defgeneric greet (instance))
(defmethod greet ((this person))
(format t "~&Hello, ~a.~%" (person-name this)))
(defparameter *john-doe* (make-instance 'person :name "John Doe"))
Call it:
(greet *john-doe*)
ResultHello, John Doe
NIL
defgeneric defines a generic function. Generic functions define an interface
for, and then dispatch to, generic methods. greet defines an interface: it
only requires an instance of a class. When a call to greet is made, it looks
at the object instance passed and dispatches to the method specialized on that
generic function.
Multiple Inheritance & Multiple Dispatch
Common Lisp's object system supports both multiple inheritance and multiple dispatch
(defclass flying ()
((max-altitude :initarg :max-altitude :initform 1000 :accessor max-altitude)))
(defclass fire-breathing ()
((flame-range :initarg :flame-range :initform 30 :accessor flame-range)))
(defclass armored ()
((armor-rating :initarg :armor-rating :initform 50 :accessor armor-rating)))
(defclass dragon (flying fire-breathing) ())
(defclass warrior (armored) ())
(defclass mage (flying) ())
Now multiple dispatch interacts with the inheritance:
(defgeneric attack (attacker target))
(defmethod attack ((attacker fire-breathing) (target armored))
(format t "Fire hits armor! ~a% deflected.~%" (armor-rating target)))
(defmethod attack ((attacker flying) (target flying))
(format t "Aerial battle at ~am altitude!~%" (min (max-altitude attacker) (max-altitude target))))
(defmethod attack ((attacker dragon) (target warrior))
(format t "The dragon dives and breathes fire on the warrior!~%")
(call-next-method)) ;; also runs fire-breathing vs armored
(defparameter *smaug* (make-instance 'dragon :max-altitude 5000 :flame-range 100))
(defparameter *aragorn* (make-instance 'warrior :armor-rating 80))
(defparameter *gandalf* (make-instance 'mage :max-altitude 200))
(attack *smaug* *aragorn*)
ResultThe dragon dives and breathes fire on the warrior!
Fire hits armor! 80% deflected.
=> NIL
(attack *smaug* *gandalf*)
ResultAerial battle at 200m altitude!
=> NIL
(attack *gandalf* *smaug*)
ResultAerial battle at 200m altitude!
=> NIL
Before, After, & Around Methods
Every generic method can be wrapped in code that will modify its behavior before it's called, after it's called, or both ("around"). Methods can be specialized on more than one type. A method call is sent to the method that is most "specific"–the method that specializes on types that are closest to the ones passed to the method as argument. A more specific method can call the next less specific method in a chain of methods.
Method combination is a technique that involves chaining together some mixture of the above techniques. Extending an OOP system in Common Lisp will involve some degree of method combination.
To modify the behavior of a method before it's called, you can make a :before method:
(defmethod greet :before ((this person))
(format t "~&Oh, what a nice looking young man. Let me go talk to him...~%"))
(greet *john-doe*)
ResultOh, what a nice looking young man. Let me go talk to him...
Hello, John Doe.
NIL
To modify its behavior after it's called, make an :after method:
(defmethod greet :after ((this person))
(format t "~&(Strong handshake, good man.)~%"))
(greet *john-doe*)
ResultOh, what a nice looking young man. Let me go talk to him...
Hello, John Doe.
(Strong handshake, good man.)
NIL
Before and after methods are fairly self explanatory. Primary methods are the core of the onion; before and after methods are two layers around the core.
Around methods are a layer outside of before and after methods. They can do
something before both entry from :before methods and after exit from :after methods.
(defmethod greet :around ((this person))
(format t "I should get off the couch and go talk to people."))
(greet *john-doe*)
ResultI should get off the couch and go talk to people.
NIL
However, they require extra work to use.
(defmethod greet :around ((this person))
(format t "I should get off the couch and go talk to people.")
(call-next-method)
(format t "Here's my card, ~a. Let me know if you need help learning Lisp." (person-name this)))
(greet *john-doe*)
ResultI should get off the couch and go talk to people. Oh, what a nice looking
young man. Let me go talk to him... Hello, John Doe. (Strong handshake, good
man.) Here's my card, John Doe. Let me know if you need help learning Lisp.
NIL
call-next-method calls the next most specific version of greet for the
*john-doe* object, which is the before method. The before method automatically
calls the next method–the primary method–so we don't have to call
call-next-method in it. Same goes for the around method.
Since call-next-method works by calling the next most specific method, it can
be used to chain together methods in other ways.
(defclass smart-person (person) ())
(defclass rich-person (person) ())
(defclass dumb-person (person) ())
(defclass poor-person (person) ())
(defparameter *musk* (make-instance 'smart-person :name "Mr. Musk"))
Without call-next-method, only the most specific primary method runs:
(defmethod greet ((this smart-person))
(format t "~&(Wow, it's ~a! I can't believe he's here!)~%" (person-name this)))
(greet *musk*)
ResultI should get off the couch and go talk to people. Oh, what a nice looking
young man. Let me go talk to him... (Wow, it's Mr. Musk! I can't believe
he's here!) (Strong handshake, good man.) Here's my card, Mr. Musk. Let me
know if you need help learning Lisp.
NIL
With call-next-method, the next most specific method also runs:
(defmethod greet ((this smart-person))
(format t "~&(Wow, it's ~a! I can't believe he's here!)~%" (person-name this))
(call-next-method))
(greet *musk*)
ResultI should get off the couch and go talk to people. Oh, what a nice looking
young man. Let me go talk to him... (Wow, it's Mr. Musk! I can't believe
he's here!) Hello, Mr. Musk (Strong handshake, good man.) Here's my card,
Mr. Musk. Let me know if you need help learning Lisp.
NIL
All this indirection can be confusing. If you are extending some OOP system and
your method isn't being called for some reason, you can use
compute-applicable-methods to find out which methods a particular combination
of arguments to a certain function name apply to those arguments.
(compute-applicable-methods #'greet (list (make-instance 'person :name "Micah")))
Result(#<STANDARD-METHOD COMMON-LISP-USER::GREET :AROUND (PERSON) {7009D091C3}>
#<STANDARD-METHOD COMMON-LISP-USER::GREET :AFTER (PERSON) {7009D091E3}>
#<STANDARD-METHOD COMMON-LISP-USER::GREET :BEFORE (PERSON) {7009D09203}>
#<STANDARD-METHOD COMMON-LISP-USER::GREET (PERSON) {7009D09223}>)
(compute-applicable-methods #'greet (list (make-instance 'smart-person :name "Mr. Musk")))
Result(#<STANDARD-METHOD COMMON-LISP-USER::GREET (SMART-PERSON) {700792E213}>
#<STANDARD-METHOD COMMON-LISP-USER::GREET :AROUND (PERSON) {7009D091C3}>
#<STANDARD-METHOD COMMON-LISP-USER::GREET :AFTER (PERSON) {7009D091E3}>
#<STANDARD-METHOD COMMON-LISP-USER::GREET :BEFORE (PERSON) {7009D09203}>
#<STANDARD-METHOD COMMON-LISP-USER::GREET (PERSON) {7009D09223}>)
Pretty Printing Objects
There are several generic functions worth knowing about when you first start
using Lisp. One of them is print-object–used for pretty printing objects.
(defmethod print-object ((this creature) stream)
(with-slots (name territory) this
(format stream "~a ~a" name territory)))
Now instances print nicely:
(make-instance 'creature :name "Cat" :territory "House")
ResultCat House
with-slots is a useful macro for dealing with object data. You could use let
instead…
(let ((name (slot-value this 'name))
(territory (slot-value this 'territory)))
...)
But with-slots is nicer for this purpose. There is also with-accessors,
which is equivalent to using creature-name and creature-territory instead of
slot-value.
In the case of print-object, it's safest to stick with with-slots or
slot-value. If you change the accessor, you might end up with difficult to
debug errors when printing objects.
Another common error would be to try to print slot values that aren't bound.
That's why you should probably have :initform set to nil if you have no
other desired default value; you won't have trouble with UNBOUND SLOT errors.
(defclass beverage ()
((name :initarg :name :accessor name)
(amount :initarg :amount :accessor amount)))
(make-instance 'beverage)
Result#<BEVERAGE {700B0D0343}>
(defmethod print-object ((this beverage) stream)
(with-slots (name amount) this
(format stream "~a ~a" name amount)))
(make-instance 'beverage)
Result#<BEVERAGE {7008470343}>
Since we didn't bind any of the slots–neither in the call to make-instance
nor in the class definition with :initform–we get the <<error printing
object>> message. We have two choices to fix the problem: check for unbound
slots in the print-object method using slot-boundp, or prevent slots from
being unbound when the object is initialized.
;; Check for unbound slots.
(defmethod print-object ((this beverage) stream)
(let ((name (when (slot-boundp this 'name) (slot-value this 'name)))
(amount (when (slot-boundp this 'amount) (slot-value this 'amount))))
(format stream "~a ~a" name amount)))
(make-instance 'beverage)
ResultNIL NIL
;; Prevent slots from being unbound by specifying a default value of nil.
(defclass beverage ()
((name :initarg :name :initform nil :accessor name)
(amount :initarg :amount :initform nil :accessor amount)))
(defmethod print-object ((this beverage) stream)
(with-slots (name amount) this
(format stream "~a ~a" name amount)))
(make-instance 'beverage)
ResultNIL NIL
Even Better Printing
By default, Lisp will provide a printed representation of an object that looks like this:
#<BEVERAGE {700B0D0343}>
This representation contains two important piece of information:
#<BEVERAGE ...>: This is aBEVERAGEobject.{700B0D0343}: This is where the object is in memory.
Consider this:
(defclass warm-beverage (beverage) ())
(defclass cold-beverage (beverage) ())
They print identically:
(values (make-instance 'warm-beverage :name "Coffee" :amount 1000)
(make-instance 'cold-beverage :name "Coffee" :amount 1000))
ResultCoffee 1000, Coffee 1000
There's no indication that there is a difference between these two objects. If they weren't shown side-by-side, we wouldn't even know they were two different objects.
To add this information back into your custom pretty printers, you can use the
print-unreadable-object macro.
(defmethod print-object ((this beverage) stream)
(print-unreadable-object (this stream :type t :identity t)
(with-slots (name amount) this
(format stream "~a ~a" name amount))))
Now instances print with type and identity info:
(make-instance 'cold-beverage :name "Coffee" :amount 1000)
Result#<COLD-BEVERAGE Coffee 1000 {7008A403A3}>
Modifying Object Initialization
Let's say we have the following defclass:
(defclass user ()
((name :initarg :name
:initform nil
:accessor user-name)
(email :initarg :email
:initform nil
:accessor user-email)))
We've specified the type of data the slots will hold: strings.
Let's run make-instance:
(make-instance 'user)
Result#<USER {7007C40343}>
It works, but what we really want is:
- Both slots to be required.
- To validate that the value assigned in
emailis actually an email.
We can do that using two important features of Common Lisp's object system: custom initializer methods and method modifiers.
To validate that the email is a valid email, we would like to be able to use
regular expressions. Lisp doesn't have regular expression support built in, so
we'll use the ppcre package to give us regular expressions:
(ql:quickload "cl-ppcre")
Then we'll make a function for validating that the string passed to
make-instance is a valid email.
(defun valid-email-address-p (string)
(not (null
(ppcre:scan "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$" string))))
We can test it:
(valid-email-address-p "[email protected]")
ResultT
(valid-email-address-p "dude*@*man.com")
ResultNIL
Now we just need some way to run this function when we run make-instance. To
do that, we can use custom initialization with the initialize-instance method.
(defmethod initialize-instance :before ((this user) &key name email)
(unless name
(error "Name is required."))
(unless email
(error "Email is required."))
(unless (valid-email-address-p email)
(error "~a is not a valid email." email)))
initialize-instance is a generic method run whenever an object is created.
Using the :before method modifier, we can run some code before the main
initialize-instance method. Here, we ensure that the name and email slots are
bound and that the email is valid. Importantly, if the data doesn't pass the
checks, the developer is thrown into the debugger, and the object is never
created.
Specializing Methods On Other Values
It's possible to specialize methods on other values besides just classes/types.
(defmethod greet ((this (eql :robot)))
(format t "Woah, this is a realistic robot."))
(greet :robot)
ResultWoah, this is a realistic robot.
NIL
(defmethod greet ((this (eql (+ 2 2))))
(format t "Hey, Micah."))
(greet 4)
ResultHey, Micah.
NIL
(defmethod greet ((this (eql #'+)))
(funcall this 1 2 3 4 5))
(greet #'+)
Result15 (4 bits, #xF, #o17, #b1111)
In the section later about structures, I'll show you that methods can even be specialized on structures and don't require you to use classes/types for your data structure.
Unfortunately, you can't specialize a method on things like the type string or
the shape of some data structure. For that, there's Coalton.
(defmethod greet ((this (eql 'string)))
(format t "What's up, ~a?~%" this))
(greet "Dude")
ResultError: There is no applicable method for the generic function
#<STANDARD-GENERIC-FUNCTION COMMON-LISP-USER::GREET (12)> when called with
arguments ("Dude").
[Condition of type SB-PCL::NO-APPLICABLE-METHOD-ERROR]
Introspection
You can get information about a class or object by various means.
class-of takes an object instance and returns a class instance.
(class-of (make-instance 'smart-person :name "Mr. Musk"))
Result#<STANDARD-CLASS CL-USER::SMART-PERSON>
find-class takes the name of a class and returns a class instance.
(find-class 'smart-person)
Result#<STANDARD-CLASS CL-USER::SMART-PERSON>
class-name returns the name of a class instance.
(class-name (class-of (make-instance 'smart-person :name "Mr. Musk")))
ResultSMART-PERSON
slot-boundp takes an object instance a slot name and returns t if that slot
is bound.
(slot-boundp (make-instance 'smart-person :name "Mr. Musk") 'name)
ResultT
slot-exists-p take an object instance and a slot name and returns t if that
slot exists.
(slot-exists-p (make-instance 'smart-person :name "Mr. Musk") 'age)
ResultNIL
HASH-TABLES
A hash-table is a group of key/value pairs. associated-lists are the
list-based equivalent.
To make a hash-table, use make-hash-table.
(make-hash-table)
Result#<HASH-TABLE :TEST EQL :COUNT 0 {7008090473}>
Common Lisp doesn't have any built-in syntax for creating hash-tables like using
{} or anything like that. In fact, you can't even define the initial contents
of the hash-table when you make it–you need separate functions for that.
To add an item to a hash-table, use setf + gethash:
(defparameter *h* (make-hash-table))
(setf (gethash :name *h*) "Micah")
ResultMicah
To access values, pass the key and hash-table to gethash.
(gethash :name *h*)
ResultMicah
(gethash :age *h*)
ResultNIL
You can modify the element using the same setf + gethash pattern.
(setf (gethash :name *h*) "Bob")
ResultBob
Compare this to alists:
(defparameter *a* '())
(setf *a* (append *a* '((:name "Micah"))))
*a*
Result((:NAME Micah))
(cadr (assoc :name *a*))
ResultMicah
(cadr (assoc :age *a*))
ResultNIL
(setf (cadr (assoc :name *a*)) "Bob")
*a*
Result((:NAME Bob))
Printing a hash-table
Hash-tables don't get printed nicely like lists do.
*h*
Result#<HASH-TABLE :TEST EQL :COUNT 1 {700AFCC1E3}>
*a*
((:NAME Bob))
Use maphash to print key/value pairs or do other mapping operations.
(defun print-hash (hash-table)
(maphash (lambda (key value) (format t "~&~a -> ~a~%" key value)) hash-table))
Now we can use it to inspect hash-tables.
(print-hash *h*)
ResultNAME -> Bob
=> NIL
(setf (gethash :age *h*) 39)
Result39
(print-hash *h*)
ResultNAME -> Bob
AGE -> 39
=> NIL
You can also use the loop macro to iterate through a hash-table, but we'll
cover that more later in the chapter on loop.
Using strings as keys
The keys of a hash-table can be keywords, strings, or symbols. But, there's a
catch:
(setf (gethash 'favorite-language *h*) "Common Lisp")
(print-hash *h*)
ResultNAME -> Bob
AGE -> 39
FAVORITE-LANGUAGE -> Common Lisp
=> NIL
(gethash 'favorite-language *h*)
ResultCommon Lisp
(setf (gethash "location" *h*) "Japan")
(print-hash *h*)
ResultNAME -> Bob
AGE -> 39
FAVORITE-LANGUAGE -> Common Lisp
location -> Japan
=> NIL
(gethash "location" *h*)
ResultNIL
make-hash-table, when searching for "location" or any other key, defaults to
testing keys using eql. As I wrote previously, eql tests if objects are
stored in the same place in memory unless their characters or numbers of the
same type.
(eql 'favorite-language 'favorite-language)
ResultT
(eql "location" "location")
ResultNIL
You can change the test to equal, equalp, or eq.
With equal, you can compare the "structure" of two objects:
(defparameter *h* (make-hash-table :test #'equal))
(setf (gethash "location" *h*) "Japan")
(gethash "location" *h*)
ResultJapan
(gethash "LOCATION" *h*)
ResultNIL
With equalp, your strings can be different cases:
(setf *h* (make-hash-table :test #'equalp))
(setf (gethash "location" *h*) "Japan")
(gethash "location" *h*)
ResultJapan
(gethash "LOCatiON" *h*)
ResultJapan
ARRAYS
One of the most fun things about Common Lisp is that it is the highest-level language to exist (thanks to macros), but it is also surprisingly low level as well.
In Lisp, a list is a linked-list. Arrays, on the other hand, are surprisingly similar to the arrays in C. You need to manually set their size at creation time. Resizing them involves either assigning them a special property or simply recreating the array with the new size.
Arrays are useful when you have a prototype using lists and need to optimize for speed and memory efficiency.
There are several different kinds of arrays. They include:
- Vectors (one-dimensional arrays)
- Multi-dimensional arrays
- Specialized arrays
- Adjustable arrays
- Strings
Because my goal is to cover the essentials for getting started with Common Lisp and arrays are largely for optimization, we won't go into great detail about arrays. However, I will talk about vectors.
Making a vector
Vectors are simple one-dimensional arrays.
make-array is the primary function for making arrays of all types.
(make-array 5)
The first argument to make-array is the dimensions of the array: how many
columns in how many rows. A single number makes a one-dimension array with the
same number of elements as the number.
You can set the initial element(s) in the array:
(make-array 5 :initial-element "ALMIGHTY")
Result#("ALMIGHTY" "ALMIGHTY" "ALMIGHTY" "ALMIGHTY" "ALMIGHTY")
(make-array 5 :initial-contents '(this is almighty common lisp))
Result#(THIS IS ALMIGHTY COMMON LISP)
You can also use the # reader macro.
(defparameter *almighty* #(this is almighty common lisp))
Accessing data in a vector
Use aref to access the data by index.
(aref *almighty* 0)
ResultTHIS
(aref *almighty* 4)
ResultLISP
(aref *almighty* 5)
ResultInvalid index 5 for (SIMPLE-VECTOR 5), should be a non-negative integer below 5.
[Condition of type SB-INT:INVALID-ARRAY-INDEX-ERROR]
This shows one of the first important differences between a list and an array in
practical use. Using nth to get an element in a list by its place in the list,
if you go beyond the last element, nth only returns NIL without an error.
(defparameter *almighty-list* '(this is almighty common lisp))
(nth 0 *almighty-list*)
ResultTHIS
(nth 4 *almighty-list*)
ResultLISP
(nth 99 *almighty-list*)
ResultNIL
Modifying data in a vector
Use setf to modify the data at a place in a vector
(aref *almighty* 3)
(setf (aref *almighty* 3) 'powerful)
*almighty*
Result#(THIS IS ALMIGHTY POWERFUL LISP)
Adding/Deleting an element of a vector
Deleting items from an array requires more manual work than with lists.
One method involves changing the size of the vector with adjust-array, leaving
no "empty" cells. This method relies on the array being created with the
:adjustable option set to t, making it an ~adjustable array=.
(defparameter *adjustably-almighty* (make-array 5 :initial-contents '(THIS IS ALMIGHTY COMMON LISP) :adjustable t))
(defun inefficiently-delete-vector-element (vector index)
(when (< index (length vector))
(loop for i from index below (1- (length vector))
;; Move all elements above the target index down one cell,
do (setf (aref vector i) (aref vector (1+ i))))
(adjust-array vector (1- (length vector)))) ; Resize the array.
vector)
Now we can use it:
(inefficiently-delete-vector-element *adjustably-almighty* 3)
Result#(THIS IS ALMIGHTY LISP)
You can set a :fill-pointer for the array:
(defparameter *fill-pointer-almighty* (make-array 5 :initial-contents '(this is almighty common lisp) :fill-pointer 5))
Use vector-pop to remove the last element of a vector that has a
:fill-pointer set. Use vector-push to add an element to the end of the
vector. It will not change the size/dimensions of the array.
(vector-pop *fill-pointer-almighty*)
ResultLISP
The data is still there if we look:
(aref *fill-pointer-almighty* 4)
ResultLISP
And the size of the array hasn't changed:
(array-dimensions *fill-pointer-almighty*)
Result(5)
But if we look at it's contents…
(loop :for word :across *fill-pointer-almighty*
:collect word)
Result(THIS IS ALMIGHTY COMMON)
…we only show contents up to the fill-pointer.
But we can add content back:
(vector-push 'java *fill-pointer-almighty*)
Result4
(print *fill-pointer-almighty*)
Result#(THIS IS ALMIGHTY COMMON JAVA)
And we can check data at the previous position:
(aref *fill-pointer-almighty* 4)
ResultJAVA
vector-push won't resize the array beyond the dimensions defined with
make-array. Use vector-push-extend to resize the array when it's necessary.
(vector-push 'dude *fill-pointer-almighty*)
ResultNIL
There's no change yet because we can't add anything past the fill pointer.
(print *fill-pointer-almighty*)
Result#(THIS IS ALMIGHTY COMMON JAVA)
And the array hasn't changed size:
(array-dimensions *fill-pointer-almighty*)
Result(5)
(vector-push-extend 'dude *fill-pointer-almighty*)
Result5
(print *fill-pointer-almighty*)
Result#(THIS IS ALMIGHTY COMMON JAVA DUDE)
To what extent the array is enlarged is implementation dependent.
(array-dimensions *fill-pointer-almighty*)
Result(10)
You can optionally specify how much to extend the array:
(vector-push-extend 'what *fill-pointer-almighty*)
(vector-push-extend 'in *fill-pointer-almighty*)
(vector-push-extend 'the *fill-pointer-almighty*)
(vector-push-extend 'world *fill-pointer-almighty*)
(vector-push-extend 'is *fill-pointer-almighty*)
(vector-push-extend 'this *fill-pointer-almighty* 1)
(array-dimensions *fill-pointer-almighty*)
(12)
Deleting an element in the middle of a non-adjustable vector requires shifting
elements that come after it down one cell and then setting the last element in
the array to NIL.
(defparameter *almighty* (make-array 5 :initial-contents '(THIS IS ALMIGHTY COMMON LISP)))
(defun delete-vector-element (vector index)
(when (< index (length vector))
(loop for i from index below (1- (length vector))
do (setf (aref vector i) (aref vector (1+ i))))
(setf (aref vector (1- (length vector))) nil)) ; Clear last element
vector)
Calling it twice shows the progressive effect:
(delete-vector-element *almighty* 3)
(delete-vector-element *almighty* 3)
Result#(THIS IS ALMIGHTY LISP NIL)
#(THIS IS ALMIGHTY NIL NIL)
This method leaves the array dimensions untouched, shifts all elements above the
given index down one cell in the array, and leaves NIL to fill the empty
space.
STRUCTURES
Structures are user-defined data types that group related data. They are similar to classes, but are faster and more memory efficient than classes under some scenarios.
Defining structures and making instances
Use defstruct to define a structure. After giving the structure a name, define
the slots. Use make-<structure name> to make an instance of the structure.
Assign slot values at instantiation using keywords.
(defstruct s-point
x
y)
(defstruct http-request
(status 200)
(headers '(()))
(body nil))
Making instances with and without keyword arguments:
(make-s-point)
(make-http-request)
(make-http-request :status 404 :body "This page doesn't exist.")
Result#S(S-POINT :X NIL :Y NIL)
#S(HTTP-REQUEST :STATUS 200 :HEADERS (NIL) :BODY NIL)
#S(HTTP-REQUEST :STATUS 404 :HEADERS (NIL) :BODY "This page doesn't exist.")
Slots default to NIL, but you can define defaults as in the http-request
example. status defaults to 200, headers to '(()), etc.
If you define the :constructor for the structure you can make slots required.
(defstruct (animal (:constructor raise-animal (name &optional territory)))
(name "")
(territory "World"))
Calling with no arguments raises an error; providing a name works:
(raise-animal)
(raise-animal "Tiger")
ResultError: invalid number of arguments: 0
#S(ANIMAL :NAME "Tiger" :TERRITORY "World")
You can also define types–checked at compile time–for slots.
(defstruct s-point
(x 0.0 :type float)
(y 0.0 :type float))
Making instances:
(make-s-point)
(make-s-point :x 5)
Result#S(S-POINT :X 0.0 :Y 0.0)
The value 5 is not of type FLOAT when setting slot X of structure S-POINT
[Condition of type TYPE-ERROR]
Structures support single inheritance.
(defstruct vehicle
make
model)
(defstruct (van (:include vehicle))
(doors 4))
Making an instance of the child structure:
(make-van)
Result#S(VAN :MAKE NIL :MODEL NIL :DOORS 4)
Accessing/Modifying slots in a structure instance
To access slot values, pass the structure to the function called
<structure-name>-<slot-name>, created when compiling the structure definition.
Use setf to modify the value.
(defstruct s-point
(x 0.0 :type float)
(y 0.0 :type float))
(defparameter *s-point* (make-s-point :x 5.0 :y 10.0))
(s-point-x *s-point*)
Result5.0
(setf (s-point-x *s-point*) 10.0)
Result10.0
(s-point-x *s-point*)
Result10.0
Printing structures
Unlike classes or hash-tables, structures print in a somewhat human-readable form.
(print *s-point*)
Result#S(S-POINT :X 10.0 :Y 10.0)
Use the :print-structure option to customize how it is printed.
(defstruct (s-point (:print-function
(lambda (this stream depth)
(format stream "~&X: ~a Y: ~a~%" (s-point-x this) (s-point-y this)))))
(x 0.0 :type float)
(y 0.0 :type float))
Now print uses the custom format:
(print (make-s-point :x 5.0 :y 10.0))
ResultX: 5.0 Y: 10.0
The depth argument in the lambda list is required. When necessary, it is used
to determine how to print nested structures.
Structures Vs. Classes
Structures and classes in Common Lisp are very similar. Given how their apparent use cases overlap, why would you choose one over the other?
For the purposes of comparison, let's set up a structure and a class that model a point on a 2D plane.
(defstruct s-point
x
y)
(defparameter *s-point* (make-s-point :x 5 :y 10))
(defclass c-point ()
((x :initarg :x
:initform nil
:accessor c-point-x)
(y :initarg :y
:initform nil
:accessor c-point-y)))
(defparameter *c-point* (make-instance 'c-point :x 5 :y 10))
Structures and classes are both user-defined data types, they both let you define groups of related data into slots, and those slots can include default values and other options.
Generic methods can be specialized on both structures and classes.
(defgeneric add (point-a point-b))
(defmethod add ((a s-point) (b s-point))
(make-s-point :x (+ (s-point-x a) (s-point-x b))
:y (+ (s-point-y a) (s-point-y b))))
(defmethod add ((a c-point) (b c-point))
(make-instance 'c-point
:x (+ (c-point-x a) (c-point-x b))
:y (+ (c-point-y a) (c-point-y b))))
(defmethod print-object ((this c-point) stream)
(print-unreadable-object (this stream :type t :identity t)
(format stream "~a ~a" (slot-value this 'x) (slot-value this 'y))))
Calling add on both types:
(add *s-point* *s-point*)
Result#S(S-POINT :X 10 :Y 20)
(add *c-point* *c-point*)
Result#<C-POINT 10 20 {700AF21593}>
However, similarities between structures and classes are superficial–the differences are much more significant.
Probably the most important difference between structures and classes in Lisp is
that when you modify their definition, currently instantiated structures aren't
updated with the new definition, but object instances are. This is called
dynamic redefinition.
Accessing slots on both:
(s-point-x *s-point*) ; => 5
(c-point-x *c-point*) ; => 5
Now, what happens to instances if we change their definitions?
With the structure:
(defstruct s-point
id
x
y)
Resultattempt to redefine the STRUCTURE-OBJECT class S-POINT
incompatibly with the current definition
[Condition of type SIMPLE-ERROR]
Restarts:
0: [CONTINUE] Use the new definition of S-POINT, invalidating already-loaded code and instances.
1: [RECKLESSLY-CONTINUE] Use the new definition of S-POINT as if it were compatible, allowing old accessors to use new instances and allowing new accessors to use old instances.
2: [RETRY] Retry SLY evaluation request.
3: [*ABORT] Return to SLY's top level.
4: [ABORT] abort thread (#<THREAD tid=21363 "slynk-worker" RUNNING {700B52AE53}>)
We need to overwrite the current definition and invalidate loaded code and
instances, or RECKLESSLY-CONTINUE.
With the class, adding an id slot compiles without trouble.
(defclass c-point ()
((id :initarg :id
:initform 7
:accessor id)
(x :initarg :x
:initform nil
:accessor x)
(y :initarg :y
:initform nil
:accessor y)))
(id *c-point*)
Result7
The existing instance is given the default initform. Just to repeat: a slot was added to an object instance simply by redefining and recompiling the class.
We can remove the id slot again:
(defclass c-point ()
((x :initarg :x
:initform nil
:accessor x)
(y :initarg :y
:initform nil
:accessor y)))
(id *c-point*)
ResultThere is no applicable method for the generic function
#<STANDARD-GENERIC-FUNCTION BOOK/ROUTES::ID (0)>
when called with arguments
(#<C-POINT 5 10 {700D71E033}>).
[Condition of type SB-PCL::NO-APPLICABLE-METHOD-ERROR]
(slot-value *c-point* 'id)
ResultWhen attempting to read the slot's value (slot-value), the slot
ID is missing from the object #<C-POINT 5 10 {700D71E033}>.
[Condition of type SB-PCL::MISSING-SLOT]
The slot and it's accessor are just gone. From the object instance.
So when you are actively developing and modifying a running program, classes provide far more flexibility.
Both structures and classes support inheritance, but classes support multiple
inheritance.
Structures can modify how print will display data using :print-function, but
classes need to use the print-object generic method.
(defstruct (s-point (:print-function
(lambda (this stream depth)
(format stream "~&X: ~a Y: ~a~%" (s-point-x this) (s-point-y this)))))
(x 0.0 :type float)
(y 0.0 :type float))
(defmethod print-object ((this c-point) stream)
(print-unreadable-object (this stream :type t :identity t)
(format stream "~a ~a" (slot-value this 'x) (slot-value this 'y))))
When you define slot types with structures, Lisp will check the type of value
passed to it at compile time. Class slot type definitions aren't enforced–you
need to enforce them yourself with initialize-instance :before.
(defclass c-point ()
((x :initarg :x
:initform 0.0
:accessor c-point-x
:type float) ; added type
(y :initarg :y
:initform 0.0
:accessor c-point-y
:type float))) ; added type
The type declaration alone does not prevent invalid values (in SBCL):
(make-instance 'c-point :x "five dot zero")
Result#<C-POINT five dot zero 0.0 {700C9969C3}>
Use initialize-instance :before to enforce types:
(defmethod initialize-instance :before ((p c-point) &key x y)
(unless (typep x 'float)
(error 'type-error :datum x :expected-type 'float))
(unless (typep y 'float)
(error 'type-error :datum y :expected-type 'float)))
(make-instance 'c-point :x "invalid" :y 2.0)
ResultThe value
"invalid"
is not of type
FLOAT
[Condition of type TYPE-ERROR]
Type definitions in class slots should be considered "documentation"–you need to enforce the types yourself. With structure slots, types are enforced by the compiler.
So which one do you choose? Generally, structures are going to be most useful for optimizing speed and memory efficiency. Classes are otherwise superior due to their more dynamic, flexible design. Unless you know for certain that you need an optimized data structure, stick with classes.

