Let’s Program A Swarm Intelligence 7: Satisfying My OCD

You’re Calling THAT Good Code!?

 

Last time we wrote a bunch of functions for randomly generating particle swarms. But it was very messy and verbose and filled with way too many calls to slot-value.

 

Now this isn’t a huge problem. The code works and having to type a few hundred extra ‘slot-value’ characters per program only wastes a few minutes of our life. From a time efficiency perspective cute cat videos are a much bigger risk to your productivity than wordy code.

 

But wordy, messy code has other downsides. More code means more places for bugs to hide and lets you fit less code on the screen at one time. Less code on the screen means lots of scrolling and lots of short-term memory abuse. It gets annoying fast.

 

So whenever possible we want to write clean, compact code that let’s us see multiple functions on one screen and leaves bugs with nowhere to hide.

 

Building Objects Using initargs Instead Of setf And slot-value

 

Remember our code for putting together a random particle? It was absolutely filled with repetitive calls to (setf (slot-value someobject someslot) somevalue). All those nearly identical lines are more than enough to make my head spin and my eyes grow all blurry.

 

So let’s get rid of them by using some of the extra features of the Lisp object system.

 

Remember that the last argument in defclass is a list of the data slots we want inside our object. Up until now we were just using simple names but we can give our classes an extra boost by wrapping those names into a list and gluing some extra keywords to them. Basically instead of just saying “I want an x-coord slot” we’re going to be saying “I want an x-coord slot with these special features”.

 

One of the more useful keywords is :initarg (don’t forget the colon, that’s Lisp keyword naming convention). :initarg let’s you create a new keyword that can be used inside of make-instance to assign a value to a data slot as soon as it is created without having to make a separate call to setf. You can name this new keyword whatever you want but it’s traditional to just name it after the data slot it’s linked to, but with a keyword colon glued to it.

 

It’s really easier to show you than to try and explain it:

 

(defclass particle-2d ()
   ((x-coord :initarg :x-coord )
    (y-coord :initarg :y-coord )
    (x-vel :initarg :x-vel )
    (y-vel :initarg :y-vel )
    history ))

 

As you can see I’ve replace our old list of data slot names with three item lists including, in order, the name of the data slot, the :initarg keyword and the new keyword I want to use in make-instance. This let’s me create a new particle with one or more data slot already filled in like this:

 

(make-instance 'particle-2d :x-coord 5 :y-coord 10 :x-vel -1.5 :y-vel 0.3)

 

Isn’t that much nicer than four separate calls to setf and slot-value?

 

Accessing Data Without slot-values

 

That little trick will already go a long way towards cleaning up generate-particle-2d and generate-swarm-2d. But that’s not the only place we have repetitive code. Take a look at print-particle-2d. The format function has four nested calls to slot-access one right after another. Not acceptable!

 

Once again Lisp already has a solution: another defclass keyword that we can use to add extra power to our particle and swarm classes. This time around we’re going to be using :accessor. The :accessor keyword has to be associated with a slot-name just like :init-args and is used to automatically create a simple function call that mimics slot-access. You can name the function whatever you want, but it usually makes the most sense to name it the same thing as the data value you want to access. Unlike with initarg this is a function, not a keyword, so we don’t want to glue a colon to it.

 

With that in mind let’s update our particle class definition again:

 

(defclass particle-2d ()
   ((x-coord
      :initarg :x-coord
      :accessor x-coord)
    (y-coord
      :initarg :y-coord
      :accessor y-coord)
    (x-vel
      :initarg :x-vel
      :accessor x-vel)
    (y-vel
      :initarg :y-vel
      :accessor y-vel)
    (history
      :accessor history )))

 

Now getting data out of a particle is as easy as (x-coord particle), which is easier to read and write than (slot-value particle ‘x-coord).

 

Default Values

 

There is one last trick that we can use to clean up our existing code and that is adding default values to some of our data slots. For example, we know that we want our swarms to always start out with a best-answer of most-positive-long-float and that we want some sort of numeric value in best-x and best-y. Doing this is as easy as slipping an :initform into our class definition:

 

(defclass swarm-2d ()
   ((best-x
      :initarg :best-x
      :accessor best-x
      :initform 0)
    (best-y
      :initarg :best-y
      :accessor best-y
      :initform 0)
    (best-answer
      :initarg :best-answer
      :accessor best-answer
      :initform most-positive-long-float)
    (particle-list
      :accessor particle-list)))

 

Now new swarms will roll off the assembly* line with their best-x, best-y and best-answer slots already filled with predictable dummy data (unless we use one of our initargs to overwrite them). That saves us a few more lines of code when generating our random swarm.

 

Code So Far

 

Now that our code is nice and tidy I think it’s time for this Let’s Program’s first complete code dump. Here are the current contents of my project file “swarm.lisp”

 

;Helper Functions
(defun ranged-random (min max)
    (+ min (random (float (- max min)))))

;Particle Related Code
(defclass particle-2d ()
    ((x-coord
        :initarg :x-coord
        :accessor x-coord)
    (y-coord
        :initarg :y-coord
        :accessor y-coord)
    (x-vel
        :initarg :x-vel
        :accessor x-vel)
    (y-vel
        :initarg :y-vel
        :accessor y-vel)
    (history
        :accessor history
        :initform () )))

(defun generate-particle-2d (x-min x-max y-min y-max)
    (let ((x-range (- x-max x-min))
          (y-range (- y-max y-min)))
      (make-instance 'particle-2d
          :x-coord (ranged-random x-min x-max)
          :y-coord (ranged-random y-min y-max)
          :x-vel (ranged-random (- x-range) x-range)
          :y-vel (ranged-random (- y-range) y-range))))

(defun print-particle-2d (particle)
    (format t "x:~a~%y:~a~%x-vel:~a~%y-vel:~a"
        (x-coord particle)
        (y-coord particle)
        (x-vel particle)
        (y-vel particle)))

(defun one-line-print-particle-2d (particle)
    (format t "pos:<~a, ~a> vel:<~a, ~a>~%"
        (x-coord particle)
        (y-coord particle)
        (x-vel particle)
        (y-vel particle)))

;Swarm Related Code
(defclass swarm-2d ()
    ((best-x
      :initarg :best-x
      :accessor best-x
      :initform 0)
     (best-y
      :initarg :best-y
      :accessor best-y
      :initform 0)
     (best-answer
      :initarg :best-answer
      :accessor best-answer
      :initform most-positive-long-float)
     (particle-list
      :accessor particle-list)))

(defun generate-swarm-2d (particle-count x-min x-max y-min y-max)
    (let ((new-swarm (make-instance 'swarm-2d)))
      (setf (particle-list new-swarm) 
            (loop for i from 1 to particle-count 
               collect (generate-particle-2d x-min x-max y-min y-max)))
      new-swarm))

(defun print-swarm-2d (swarm)
    (format t "Best input:<~a, ~a>~%Best answer:~a~%"
        (best-x swarm)
        (best-y swarm)
        (best-answer swarm))
    (loop for particle in (particle-list swarm)
       do (one-line-print-particle-2d particle)))

 

And here is a quick test showing off how everything works by generating a random four particle swarm with x-values between -10 and 10 and y values between -5 and 5:

 

[1]> (load “swarm.lisp”)

;; Loading file swarm.lisp …

;; Loaded file swarm.lisp

T

[2]> (print-swarm-2d (generate-swarm-2d 4 -10 10 -5 5))

Best input:<0, 0>

Best answer:8.8080652584198167656L646456992

pos:<-3.4808707, 0.3767519> vel:<-1.4654808, -3.5502791>

pos:<-2.8356638, 3.5513773> vel:<15.891026, 0.40564632>

pos:<-4.020668, -0.7705631> vel:<0.81866837, -5.2009716>

pos:<-3.3210301, -1.3617878> vel:<-16.749294, 4.1957817>

NIL

 

We’re Just Getting Started

 

Our code is clean, compact and we can generate an entire swarm with a single function call. That’s a pretty important milestone. From here we can finally start moving on to the fun stuff, like data visualization and the actual particle optimization algorithm.

 

 

* No pun intended, which is too bad because that would have been a great computer joke if I had thought of it on purpose.