The Common Lisp Object System (CLOS) was developed to create an object-oriented extension for the Common Lisp language. CLOS became the first ANSI standardised object-oriented language. CLOS enables definining objects such as classes and associated functions which can be helpful for defining behaviour in applications and enabling inheritance and data sharing between objects.1 There are many features of CLOS but this post will keep things fairly simple, by creating a class and defining some functions to keep track of the score for a bowling game.

The functions and game logic are adapted from The Bowling Game Kata by Robert Cecil Martin, which was written in Java and designed to explain test-driven development (TDD) principles. We will re-implement this bowling game here but in Common Lisp using CLOS.

Example of a CLOS Class

To define a new class, the macro defclass can be used, which takes a list of optional super classes, as well as a list of slot specifications and class specifications:

defclass class-name list-of-super-classes
    list-of-slot-specifications class-specifications

For our example game, we will define a class named bowling-game. We will leave the list of super classes empty, i.e. we will not inherit from other classes. We will specify three slots: total, rolls, and current-roll, which will help us to track the game state. Specifically, the slot total will be used to store the total score of the game; the slot rolls will be used to store the number of pins knocked down in each roll, and the slot current-roll will be an index to track the current roll in the game.

In the list of slot specifications, the :initarg form is used to set the variable name used when initialising the class and the :accessor form is used to set the variable name when accessing the class. We will use the same name for these variables as the slot name as it means that we will not need to keep track of different names in the code. The :initform variable is used to set the initial value of the slot, provided no :initarg argument is passed to the function make-instance (which will be described later) that is used to create a new instance of the class.

For our bowling-game class, we will initally set the total to 0, and we will do the same for the index current-roll. For rolls, we will later define a method for the generic function initialize-instance which can be used to specify actions to be taken when an instance is initialised. As such, we will just provide an :accessor argument for this slot for the time being. Finally, there will be a class specification for the documentation string:

(defclass bowling-game ()
  ((total
    :initarg :total    ;; name for slot when initialising
    :initform 0        ;; initial value of variable
    :accessor total)   ;; name for slot when accessing
   (rolls
    :accessor rolls)
   (current-roll
    :initarg :current-roll
    :initform 0
    :accessor current-roll))
  (:documentation "Score a bowling game"))

We can create an instance of our class by passing the class name as an argument to the make-instance function:

(setf new-game (make-instance 'bowling-game))

We can see various details about an instance by calling describe on our instance:

(describe new-game)

#<BOWLING-GAME {10031C0E53}>
  [standard-object]

Slots with :INSTANCE allocation:
  TOTAL                          = 0
  ROLLS                          = #<unbound slot>
  CURRENT-ROLL                   = 0

We can see that the initial values for the slots total and current-roll are set to 0, and the value for the slot rolls is currently unbound because we did not provide an :initform argument for this slot in the class definition. If we wanted to change the initial value for one of our slots, we can do so by passing the name used for the :initarg of that slot to make-instance. Let’s say we had a beginner player and we wanted to give them a slight advantage by adding 50 to their initial total score:

(setf easy-game (make-instance 'bowling-game :total 50))

Generic Functions

Once we have created our bowling-game class, we can define some functions that can be used by it. A generic function is a lisp function with an associated set of methods and dispatches them when it’s invoked.2 In contrast to an ordinary function defined with defun, a generic function may have multiple methods with the same name, and a specific method is run based on whether the arguments passed to the method fulfil certain criteria. Constraining methods to be only run when the appropriate arguments are passed means that the method is “specialized” and when there are multiple methods with the same name, the specialized method will take precedence over the default one. In CLOS, methods can be specialized for more than one argument or class (in which case, it is referred to as a multi method). A benefit of this is that methods do not have to live inside their classes.

Methods can be created using the defmethod macro. Note that the first time we define a method using defmethod with a given function name, a generic function is automatically created with that name as well, which will contain this method as well as any others that we define. In our functions, we will introduce a specializer that will ensure that the argument g passed to the function is a class or subclass of the bowling-game class:

(defmethod foo ((g bowling-game)) ;; specializer
  function-body)

We mentioned that we can initialise the values for certain slots in the class definition. However, if one needs more control over the initialisation, we can define a method on the generic function initialize-instance, which is called by make-instance. We will create a method to initialise our rolls slot. Specifically, we can define an :after method on initialize-instance that sets the value of the rolls slot to be a list containing 21 zeros as a game of bowling can contain up to 21 rolls: each game consists of 10 frames, with 2 rolls per frame. If the player rolls a spare or a strike in the tenth frame, they get an extra roll to complete the spare/strike.

(defmethod initialize-instance :after ((g bowling-game) &key)
  (setf (rolls g) (make-list 21 :initial-element 0)))

In the above, we are specialising our method to accept an argument g that must be a class or subclass of bowling-game. The &key argument is used in the initialize-instance method to maintain consistency between the parameter list of this method and the generic function initialize-instance. We can access the value of the rolls slot of the class g (representing an instance of our bowling-game class) using (rolls g). Slot values can be changed using the setf special form. Now if we create a new instance of our game, this initialize-instance method will be called to set the value of rolls to be 21 zeros:

(setf new-game (make-instance 'bowling-game))

(rolls new-game) => (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)

Now that we have initialised our class with the right values, we will introduce a number of functions that will keep track of the game state, i.e. update the game after each roll, and calculate the score of the game after it is complete. We will also introduce some functions that will be useful for the control flow in our game, and will help us to avoid cluttering the main scoring function with conditional expressions and list-indexing operations.

In the following paragraphs we will describe each of these functions (as well as some of the Common Lisp special forms they use):

The function roll will set the element of rolls at the index current-roll to the number of pins that were knocked down. Again, we will include a specializer to ensure the argument g is of type bowling-game but we will also pass an additional argument pins that denotes the number of pins knocked down in the current roll (the value for pins should be an integer in the range of 0 - 10, but right now, our program is not ensuring that).

Given that we are passing an instance of the class bowling-game to this function, we can access the slot values of this class with the macro with-accessors, passing it a list of slot variables we would like to access in this function, from the class g. Specifically, in each list, e.g. (rolls rolls) or (current-roll current-roll), the first element of each list is a local variable bound to the value of the second element, which is the value of the slot with that given name, for the instance g of bowling-game. Once these variables are bound inside the with-accessors macro, we can directly read from them or write to them. We can use the special form elt to access the element of a list at a specific index, e.g. (elt '(1 2 3) 0) => 1. We use setf to set the appropriate element in the list to the number of pins knocked down. Finally, we can increment the current-roll slot using the incf special form which by default, adds one to the current value of the variable:

(defmethod roll ((g bowling-game) pins)
  (with-accessors ((rolls rolls)
		   (current-roll current-roll))
      g
    (setf (elt rolls current-roll) pins)
    (incf current-roll)))

We will briefly describe the helper functions which will be used to obtain the number of pins knocked down at various frames. The function sum-of-rolls-in-frame receives an additional argument frame-index – the index of the current frame and adds together the values for the first and second rolls of the frame. We will use the special form nth to access the nth value of a 0-indexed list, e.g. (nth 2 '(a b c d e)) => C:

(defmethod sum-of-rolls-in-frame ((g bowling-game) frame-idx)
  (with-accessors ((rolls rolls))
      g
    (+ (nth frame-idx rolls) (nth (+ frame-idx 1) rolls))))

The function strike-bonus also receives the frame-idx argument and uses it to add together the values of the next two rolls. We will later see that if a strike is rolled, the player gets an extra 10 points added to the number of pins knocked down in the next two rolls.

(defmethod strike-bonus ((g bowling-game) frame-idx)
  (with-accessors ((rolls rolls))
      g
    (+ (nth (+ frame-idx 1) rolls) (nth (+ frame-idx 2) rolls))))

The function spare-bonus is similar to strike-bonus except that it returns the number of pins knocked down in the first roll of the next frame:

(defmethod spare-bonus ((g bowling-game) frame-idx)
  (with-accessors ((rolls rolls))
      g
    (nth (+ frame-idx 2) rolls)))

The function is-strike returns True if the current roll is equal to 10 and Nil otherwise. Similarly, the function is-spare returns True if the current frame is equal to 10 and Nil otherwise. Both function use the = special form which can be used to compare if two integers are equal, e.g. (= 1 1) => T and returns Nil otherwise, e.g. (= 1 2) => NIL

(defmethod is-strike ((g bowling-game) frame-idx)
  (with-accessors ((rolls rolls))
      g
      (= (nth frame-idx rolls) 10)))


(defmethod is-spare ((g bowling-game) frame-idx)
  (with-accessors ((rolls rolls))
      g
    (= (+ (nth frame-idx rolls) (nth (+ frame-idx 1) rolls)) 10)))

The function score will contain the main logic for the game. For each frame, we can check whether the frame is a strike, a spare or a regular roll (if strike do … elif spare do … else do). We will briefly describe how we can achieve this behaviour in Common Lisp. In Common Lisp, there is no elif statement. When an if statement is used, the first expression below it will be run if the condition is True and the second expression will be run if the condition is False or Nil:

(if (= (+ 1 1) 2)
	 (format t "numbers add to 2")            ;; True 
	 (format t "numbers do not add to 2"))    ;; False

We can add the equivalent of an elif statement by placing another if statement in the expression that is called in the case of the first if statement returning a False. We can achieve the behaviour of if-elif-else by writing the else expression we want to run in the False expression of the nested if. One other thing to note is that we can use the progn special form to allow multiple expressions to be evaluated for each True or False expression:

(if (= (+ 1 1) 2)
	 (progn                                   ;; True
	   (format t "numbers add to 2~%")
	   (format t "writing more text..."))
	 (progn                                   ;; False
	   (format t "numbers do not add to 2~%")
	   (format t "writing more text...")))

Now that we have described how to check if the current frame is a strike, a spare or a regular frame, we will describe the full score function.

We will use the loop macro to loop over the 10 frames of the game, also initialising a variable frame-idx, initially set to 0, which will point to the appropriate index of our rolls. At each iteration of the loop, we will check whether the frame at frame-idx is a strike, a spare or a regular roll. If the frame is a strike, we will add 10 to the next two rolls and increment frame-idx by one position to get to the next frame. If the frame is a spare, we will add 10 to the first roll in the next frame and increment frame-idx by two positions to get to the next frame. Otherwise, we will just add the number of pins knocked down in the current frame and increment frame-idx by two positions to get to the next frame:

(defmethod score ((g bowling-game))
  (with-accessors ((total total))
      g
    (loop with frame-idx = 0
	  for frame from 1 to 10 do
	    (if (is-strike g frame-idx)
		(progn
		  (incf total (+ (strike-bonus g frame-idx) 10))
		  (incf frame-idx 1))
		(if (is-spare g frame-idx)
		    (progn
		      (incf total (+ (spare-bonus g frame-idx) 10))
		      (incf frame-idx 2))
		    (progn
		      (incf total (sum-of-rolls-in-frame g frame-idx))
		      (incf frame-idx 2)))))))

Tying it all together

To tie everything together, we will define two ordinary functions, create-game and test. The function create-game uses the make-instance macro to make an instance of our bowling-game class:

(defun create-game ()
  (make-instance 'bowling-game))

Finally, the test function will run through all of the necessary methods for scoring our game:

(defun test ()
  (let ((x '())
	(game '(3 2 3 7 10 10 10 10 4 3 3 3 3 3 3 7 6)))
    (setq x (create-game))
    (loop for r in game do
      (roll x r))
    (score x)
    (describe x)))

In the test function, we also use the let special form to create local variables x and game, where game is an example game consisting of a list of rolls: '(3 2 3 7 10 10 10 10 4 3 3 3 3 3 3 7 6). We then set x be an instance of bowling-game, and when the make-instance function is ran within create-game, it will run our initialize-instance method to initialise the slot value of rolls. We then loop over each roll in the sample game, and call the roll function to update the state of the game with the value of that roll. After the rolls are processed, we can call the score function to count the overall score of the game. In this case, the code should correctly score the game with a value of 161. We can analyse the state of the game using (describe x):

#<BOWLING-GAME {1003B04AD3}>
  [standard-object]

Slots with :INSTANCE allocation:
  TOTAL                          = 161
  ROLLS                          = (3 2 3 7 10 10 10 10 4 3 3 3 3 3 3 7 6 0 0 0 0)
  CURRENT-ROLL                   = 17

We can can also change the rolls for game in test to be a perfect game of 12 strikes '(10 10 10 10 10 10 10 10 10 10 10 10), in which case we should get a maximum score of 300. Running test again:

#<BOWLING-GAME {1003B5F993}>
  [standard-object]

Slots with :INSTANCE allocation:
  TOTAL                          = 300
  ROLLS                          = (10 10 10 10 10 10 10 10 10 10 10 10 0 0 0 0 0 0 0 0 0)
  CURRENT-ROLL                   = 12

Thanks for reading!

References

1 source: https://www.dreamsongs.com/Files/clos-cacm.pdf

2 source: http://cl-cookbook.sourceforge.net/clos-tutorial/

Document History

23/09/2022 - Thanks to users on /r/lisp for their feedback, which improved the code and for pointing out that we can directly read/write to variables within the with-accessors macro.

26/09/2022 - Now using an initialize-instance method with an :after qualifier to initialise the value for rolls. Also changed wording to mention “generic functions”, and noted that they contain methods.