3LISP FUNDAMENTALS

LISP FUNDAMENTALS

Now that you've gotten your feet wet with installing and using Doom Emacs, we can get to work on learning Common Lisp.

To get started, open a file/buffer to almighty/lisp-fundaments.lisp (File -> Visit New File) and follow along. You'll probably be prompted to create some directories. When you open it, also open a REPL (SLY -> mREPL -> Go to default repl) to run the code. Some of the code won't have any noticeable effect if you don't have the REPL open. While following this book, it's a good idea to always have the REPL open along with the .lisp files we create.

Lisp comes from a lineage of functional/applicative programming. That means that among the fundamentals we won't be seeing object-oriented programming stuff–that comes later. Perhaps the most important section is the one on Lists. They are the core data and code structure primitive.

To get some practice writing Lisp in Emacs you'll make a game of tic-tac-toe. Nothing fancy, something just interesting enough to let you play with some of the core concepts.

3.1LISP FUNDAMENTALS

SYNTAX & GRAMMAR

Common Lisp's syntax and grammar is very simple. It has two distinctive features: all those parentheses, and prefix notation. This is a Common Lisp s-expression.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(+ 3 4)

S-expressions are made of indivisible units called atoms–such as the symbol + and the number 3–and divisible units called lists. They are also called forms.

The first atom in a list is looked up and evaluated as a function, macro, or special operator name. The rest of the arguments are evaluated before being passed to the function.

Symbols, other than the first one in a list, are evaluated as variables.

Some atoms, like numbers, keywords, or strings, evaluate to themselves.

777
Returns
777
:keywords-are-symbols-that-begin-with-colon
Returns
:KEYWORDS-ARE-SYMBOLS-THAT-BEGIN-WITH-COLON
"This is a string."
Returns
This is a string.

Some forms have special evaluation rules and are called special forms. The conditional if form is one such special form.

All inner forms are first evaluated left-to-right before being passed to a function.

3.1.1LISP FUNDAMENTALS

Prefix Notation

Math operations in Common Lisp are probably different than what you're used to.

Common Lisp math operations use prefix notation: the math operator is a function that comes at the beginning of the parenthesis, and all numbers afterward evaluated using that operator, from left to right. In school, we learn math using infix notation, where the operators are placed between each number.

(+ (* 5 5 5) (/ 18 2) (- 20 3)) ; using infix notation: 5 * 5 * 5 + 18 / 2 + 20 - 3
3.1.2LISP FUNDAMENTALS

More complicated s-expression

Let's look at a slightly more complicated example of Common Lisp syntax:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(defun cube (x)
  (* x x x))

After the opening parenthesis comes defun, a built-in macro. defun has different evaluation rules compared to regular function calls. It's used to define a function. The function above is given the name cube. After giving the function a name, we define its parameters, the data it takes as arguments to run the operations inside the function. The function takes one argument: x.

Notice that cube isn't evaluated as a variable. The x in (x) isn't initially interpreted as a function to be called.

defun's first argument is a symbol designating the name to give the function, and the second argument is a list of arguments that the function is expected to receive when it's called. The forms that follow afterward are called the body of the function.

3.2LISP FUNDAMENTALS

SYMBOLS

Symbols are atomic units that represent and point to some other data.

Under normal use, symbols in Common Lisp are case-insensitive. my-symbol and MY-SYMBOL and mY-sYmBoL are all the same.

By calling quote or using the reader-macro ', you can return the symbols, rather than evaluating and returning the values bound to the symbols.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

'my-num
Returns
MY-NUM
Notes

The single-quote character is a reader-macro equivalent to the quote special operator.

Quoting things in general is likely to be both a little confusing but also very important later, especially when you learn macros.

3.2.1LISP FUNDAMENTALS

Introducing Global Variables

Common Lisp supports both globally and lexically scoped variables. There are several ways to define variables.

Use defvar to introduce a global variable.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(defvar *debug* nil)
*debug*
Returns
NIL

To rebind a value to a defvar variable, you need to call setf on the variable. To change the initial value, you can't simply redefine the defvar and recompile the form.

(defvar *debug* t)
*debug*
Returns
NIL

Still nil.

You have to use setf to modify the value of a defvar variable.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(setf *debug* t)
*debug*
Returns
T
Notes

setf is a general value assignment/reassignment macro.

Use defparameter to introduce a global variable with a starting value that can be modified by recompiling the defparameter form.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(defparameter *debug* nil)
*debug*
Returns
NIL
(defparameter *debug* t)
*debug*
Returns
T

Global variables defined with defvar and defparameter are surrounded with * by convention.

Use defconstant to introduce a global variable that has a value that can't be modified.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(defconstant +pi+ 3.14)
+pi+
Returns
3.14
Notes

Redefining a constant defined with defconstant has undefined consequences.

(setf +pi+ 42)
Signals
+PI+ is a constant and thus can't be set.

If you try to recompile the defconstant with a different value, you'll be given the option to redefine the constant via a restart.

(defconstant +pi+ 42)
Returns
The constant +PI+ is being redefined (from 3.14 to 42)

Constant variable names are surrounded with + by convention.

3.2.2LISP FUNDAMENTALS

Introducing Local Variables

Use let to introduce or reassign a local variable.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(let ((name 'micah))
  name)
Returns
MICAH

You can also reassign a global variable temporarily.

(defparameter *var* 'outside)
*var*                   
Returns
OUTSIDE
(let ((*var* 'inside))  
  *var*)              
Returns
INSIDE

And the value outside the let will remain.

*var*
Returns
OUTSIDE

If you need to use a variable defined within a let within the same let form then you need let*.

This let form will break:

(let ((data 'data)
      (clean-data data))
  clean-data)
Signals
The variable DATA is unbound.

Change the let to let* to make it work properly:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(let* ((data 'data)
       (clean-data data))
  clean-data)
Returns
DATA
3.2.3LISP FUNDAMENTALS

More than just variables

While variables are an important kind of symbol, they aren't the only kind of symbol.

One example of a non-variable symbols are keywords. Keywords are any symbol that begins with : (colon). Keywords evaluate to themselves; they don't hold some other value.

:this-is-a-keyword
Returns
:THIS-IS-A-KEYWORD

Keywords are used especially in property-lists and associative-lists as the key in a key/value relationship.

Function names, class names, etc. are all symbols, too. Symbols (besides keywords) are themselves a data structure and can hold more than one piece of information.

There are several functions for getting at the data saved in a symbol.

Function Return Value
symbol-function The function object saved in the symbol.
symbol-name An uppercase string with the name of the symbol.
symbol-package An instance of the package object where the symbol is interned.
symbol-value The value saved to a symbol as a variable.
symbol-plist The property-list saved to the symbol.

While most of the time you'll use defparameter, let, defun, etc. to define symbols, sometimes you'll need to do symbol surgery, constructing and interning symbols in manually. Some of the above functions will come in handy for those kinds of operations.

Now that you know that let and let* are nearly the same, with let* providing more functionality than let, you are probably tempted to never use let. However, it is good style to be specific. If you don't need to refer to variables bound earlier in the let form, don't use let*. This prevents readers of your code from being confused.

3.3LISP FUNDAMENTALS

FUNCTIONS

On the subject of functions: Functions describe the world. Or at least that's what some insane Haskell-loving university teacher once told me.

You can define functions with defun:

(defun sum (x y) 
  (+ x y))

All functions defined with defun are global in scope. For example, compile-food-ingredients will not be lexically scoped to categorize-food, it will be available at the top level.

(defun categorize-food (food)
  (defun compile-food-ingredients (food) ...)
  ...)

To define locally-scoped named functions, use flet or labels:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(defun square-then-double (x)
  (flet ((square (y)
           (* y y)))
    (* 2 (square x))))

(defun square-then-double-with-labels (x)
  (labels ((square (y)        
             (* y y))
           (double-squared (z)
             (* 2 (square z))))
    (double-squared x)))

flet is like let, and labels is like let*. Importantly, this means if you want to do anything recursive you need to use labels.

The second parameter to defun is called a lambda list. Parameters for the function being defined are specified inside the lambda list.

3.3.1LISP FUNDAMENTALS

Parameters

There are several different kinds of parameters you can define. The most common will be required parameters. Parameters defined without any other options are required.

(defun my-fun (this-is-required))

If you don't want a parameter to be required, you can use the &optional lambda list keyword to make it optional.

(defun fun-with-optional (a &optional b)
  (format nil "The values passed: ~a and ~a.~&" a b))

(fun-with-optional 5)
Returns
The values passed: 5 and NIL.

The default value for optional arguments not passed is nil. You can set the default value.

(defun fun-with-optional (a &optional (b 10))
  (format nil "The values passed: ~a and ~a.~&" a b))

(fun-with-optional 5)
Returns
The values passed: 5 and 10.

You can create a predicate that will return whether the value was supplied or the default is being used.

(defun fun-with-optional (a &optional (b 10 b-supplied-p))
  (if b-supplied-p
      (format nil "The values passed: ~a and ~a (passed by user).~&" a b)
      (format nil "The values passed: ~a and ~a (default).~&" a b)))

By convention the predicate is named *-supplied-p, with * being the name of the parameter. Its value is t if the user supplied an argument in that position, otherwise it defaults to nil.

Using the default value of the optional argument:

(fun-with-optional 5) 
Returns
The values passed: 5 and 10 (default).

With a value passed to the optional argument position:

(fun-with-optional 5 10)
Returns
The values passed: 5 and 10 (passed by user).

All parameters following the &optional lambda list keyword will be optional. This is true of all lambda list keywords.

(defun fun-with-lots-of-options (&optional (a 1) (b 2) (c 3))
  (+ a b c))

If you want an argument to be both optional, but also want the user to be able to set the argument in any position by name, then you can use the &key keyword.

(defun fun-with-keys (&key a b c)
  (+ a b c))

(fun-with-keys :a 1 :b 2 :c 3)         

As with &optional, you can specify default values.

(defun fun-with-keys (&key (a 1) (b 2) (c 3))
  (+ a b c))

*-supplied-p parameters can also be specified. If specified, a *-supplied-p parameter will be either t or nil depending on if the user passed a value for the keyword.

(defun fun-with-keys (&key (a 1 a-supplied-p) (b 2 b-supplied-p) (c 3 c-supplied-p))
  (format nil "~a (~a) + ~a (~a) + ~a (~a) = ~a~&"
          a a-supplied-p
          b b-supplied-p
          c c-supplied-p
          (+ a b c)))

(fun-with-keys :a 5 :c 7)
Returns
5 (T) + 2 (NIL) + 7 (T) = 14

You can collect arbitrary arguments into a list with &rest.

(defun add (&rest args)
  (apply #'+ args))

(add 1 2 3 4 5)
Returns
15

It is possible to mix the different kinds of parameters.

As a general rule, you don't want to mix &optional and &key parameters.

(defun do-not-mix (&optional (a 1) (b 2) &key (c 3))
  (+ a b c))

(do-not-mix :c 5)                       
Signals
The value
  :C
is not of type
  NUMBER

Use one or the other, but not both.

You can mix &optional and &rest. Mixing &key and &rest will result in an error if you put &key before &rest, but it's okay to put the &rest before the &key.

(defun key-before-rest (&key my-key &rest args)
  (print my-key))
(key-before-rest :my-key "&key before &rest, you'll have a bad time.")
Signals
Execution of a form compiled with errors.
Form:
  #'(SB-INT:NAMED-LAMBDA KEY-BEFORE-REST
      (&KEY MY-KEY &REST ARGS)
    (BLOCK KEY-BEFORE-REST (PRINT MY-KEY)))
Compile-time error:
  misplaced &REST in lambda list:
                               (&KEY MY-KEY &REST ARGS)
   [Condition of type SB-INT:COMPILED-PROGRAM-ERROR]
(defun rest-before-key (&rest args &key my-key)
  (print my-key))

(rest-before-key :my-key "&rest before &key, you're okay.")
Returns
&rest before &key, you're okay.

If you want named arguments in your &rest args, you can supply key-value pairs in the &rest argument and manually parse them yourself.

(defun rest-args-as-plist (&rest args)
  args)

(rest-args-as-plist :a 1 :b 2 :name 'micah)
Returns
(:A 1 :B 2 :NAME MICAH)

We'll see a bit later what's going on in loop below, but this is just to say that you can have an arbitrary number of key-value pairs in your &rest argument.

(loop :with args := (rest-args-as-plist :a 1 :b 2 :name 'micah)
      :for (k v) :on args :by #'cddr
      :when (eql k :name)
        :do (return v))
Returns
MICAH
3.3.2LISP FUNDAMENTALS

Return Values

In Common Lisp, the value returned by the last expression called in a form will be the return value.

(defvar *some-var*)
(defun do-a-bunch-of-stuff ()
  (setf *some-var* "I assigned a value to *some-var*.")
  (let ((num 5))
    (square-then-double num)
    (random 10)
    "I am the value that this function will return"))
(do-a-bunch-of-stuff)

There are cases, however, when you might want to do an early return, especially when looping. When using loop, you can do early returns with return. In other cases, use return-from.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(defun early-return-example ()
  (let ((nums '(2 2 4 4 6 6 7 8 8 8)))
    (dolist (n nums)
      (when (oddp n)
        (return-from early-return-example (format nil "~a is not even." n))))))

(early-return-example)
Returns
7 is not even.
3.3.3LISP FUNDAMENTALS

Returning Multiple Values

It's possible to return multiple values using the values function.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(defun return-a-bunch-of-stuff ()
  (values
   (+ 2 7)
   (* 7 7)
   (/ 100 25)))
(return-a-bunch-of-stuff)
Returns
9, 49, 4
3.3.4LISP FUNDAMENTALS

Binding Multiple Values

A simple let won't bind all of the values returned by a function that returns multiple values. Instead, only the first value will be bound.

(let ((val (return-a-bunch-of-stuff)))
  val)
Returns
9

If you need to bind multiple values, use multiple-value-bind.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(multiple-value-bind (a b c)
    (return-a-bunch-of-stuff)
  (format t "~a * ~a * ~a = ~a" a b c (* a b c)))
Returns
9 * 49 * 4 = 1764 => NIL
3.3.5LISP FUNDAMENTALS

Breaking Lists Into Multiple Values

Sometimes you might want to take a list and break it into pieces–called destructuring. For that, there's destructuring-bind.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(destructuring-bind (a b c)
    (list 1 2 3)
  (format t "~a * ~a * ~a = ~a" a b c (* a b c)))
Returns
1 * 2 * 3 = 6 => NIL

This destructuring can be done on arbitrarily deep trees of cons cells.

(defparameter *deep-tree* `(defun function-name (a lambda-list)
                            (let ((b :some-value))
                              b)))

(destructuring-bind (the-defun the-function-name (the-a the-lambda-list)
                     (the-let ((the-b the-value-bound-to-b))
                      the-b-at-the-end))
    *deep-tree*
  the-value-bound-to-b)
Returns
:SOME-VALUE
3.3.6LISP FUNDAMENTALS

Pass by Value

When a variable is passed to a function, a new, local variable is introduced with the value of the variable passed to the function.

(defvar *some-num* 5)
(defun add-5 (num) ; local variable num is introduced
  (setf num (+ num 5)))

num is assigned the value of *some-num*.

(add-5 *some-num*)

*some-num*
Returns
5, not 10

If you want to modify the value of the global variable, you need to use setf on the global variable directly.

(defun add-5 ()
  (setf *some-num* (+ *some-num* 5)))

(add-5)

*some-num*
Returns
10
3.3.7LISP FUNDAMENTALS

First-Class Functions

Common Lisp functions are first-class. Many of Lisp's functions can take functions as arguments.

To pass a function as an argument, you have a few options.

You can use function:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(mapcar (function square-then-double) '(1 2 3 4 5))
Notes

#' is the reader-macro shorthand for the function special operator.

Or, more commonly, use the reader macro #':

(mapcar #'square-then-double '(1 2 3 4 5))

The funcall and apply functions take a function name for their first argument, applying the function to the rest of the arguments.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(funcall #'+ 1 2 3)
Returns
6
Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(apply #'+ '(1 2 3))
Returns
6
3.3.8LISP FUNDAMENTALS

Anonymous Functions

You can use anonymous functions with lambda:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

((lambda (x) (* x x)) 5)
Returns
25

Lambdas are useful when using functional programming functions like mapcar or remove-if-not.

(mapcar (lambda (x) (+ (* x x) (* x x))) '(1 2 3 4 5))
Returns
(2 8 18 32 50)
3.3.9LISP FUNDAMENTALS

Comments & Feature Flags

3.4LISP FUNDAMENTALS

LISTS

Lists are the most flexible and fundamental data structure in Common Lisp.

The easiest way to make a list is with the list function:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(list 'this 'is 'a 'list)

That list contains four symbols. The more common way to create the above list is to use quote:

(quote (this is a list))

Or, using a reader macro:

'(this is a list)

If you are experienced in other languages like Python or JavaScript, you might think Lisp's lists are the same as in those languages. However, that isn't the case.

The more fundamental data structure that lists are built on top of are cons cells. Lisp's lists are linked lists of cons cells.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(cons 'this (cons 'is (cons 'a (cons 'linked (cons 'list nil)))))

A cons cell has two parts or slots: a car and a cdr. The car contains some data, and the cdr contains either more cons cells or nil. nil terminates the linked list branch.

More importantly, and maybe confusingly, Common Lisp code is written using these very same cons cells.

;;; Parenthesis, then function name "cons", then data, all establishing a nested
;;; linked list.
(cons 'defun (cons 'sum (cons (cons 'x (cons 'y nil))
                              (cons
                               (cons '+
                                (cons 'x (cons 'y nil)))
                               nil))))
Returns
(DEFUN SUM (X Y) (+ X Y))

The result is the literal representation of this code:

(defun sum (x y)
  (+ x y))

Quoting the sum function definition will produce the same result

(quote (defun sum (x y)
         (+ x y)))
Returns
(DEFUN SUM (X Y) (+ X Y))

Importantly, that's different from merely returning a string with the code:

"(defun sum (x y)
   (+ x y))"
Returns
"(defun sum (x y)
   (+ x y))"

How is it different? Because I can evaluate the quoted code, and I can evaluate the version manually created with cons:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(eval (quote (defun sum (x y)
               (+ x y))))

(eval
 (cons 'defun (cons 'sum (cons (cons 'x (cons 'y nil))
                               (cons
                                (cons '+
                                      (cons 'x (cons 'y nil)))
                                nil)))))

The line between "code" and "data" that is clearly drawn in other languages like Python or JavaScript does not exist in Lisp, owing to the fact that code and data both share the same syntax and data structure.

3.4.1LISP FUNDAMENTALS

Basic List Functions

Because lists are so fundamental in Lisp, there are many functions for manipulating lists.

length returns how many items are in the list:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(length '(Gerald Sussman stole my wife and kicked my dog that skallywag))
Returns
11

reverse returns a new list that has the order of the elements of the initial list reversed:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(reverse '(1 2 3 4 5))
Returns
(5 4 3 2 1)

first, secondtenth get items in a list based on their position in the list:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(let ((my-list '(common lisp is a general purpose multi-paradigm programming language)))
  (list (first my-list)
        (second my-list)
        (ninth my-list)))
Returns
(COMMON LISP LANGUAGE)

member searches for a single item in a list. If it finds the item, it returns a list where the first item is the item searched for, and then the rest of the items that come after it:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(member 'general '(common lisp is a general purpose multi-paradigm programming language))
Returns
(GENERAL PURPOSE MULTI-PARADIGM PROGRAMMING LANGUAGE)

nth selects individual items by their index:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(nth 4 '(common lisp is a general purpose multi-paradigm programming language))
Returns
GENERAL

position tells you the index of an item in a list:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(position 'general '(common lisp is a general purpose multi-paradigm programming language))
Returns
4

append combines two lists:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(append '(this list is a list) '(a list))
Returns
(THIS LIST IS A LIST A LIST)
3.4.2LISP FUNDAMENTALS

Lists as Key/Value Pairs

Lists can be used as key/value pairs, called property lists, using keywords.

(defvar *micah* (list :name 'Micah :age 40))
Returns
*MICAH*

Use getf to get the value for a certain key.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(getf *micah* :age)
Returns
40

Combine setf with getf to reassign a value for a key.

(setf (getf *micah* :age) 99)
*micah*
Returns
(:NAME MICAH :AGE 99)
3.4.3LISP FUNDAMENTALS

Lists as Trees

Lists are a very simple data structure capable of making other data structures. Consider the linked list of cons cells above:

(cons 'defun (cons 'sum (cons (cons 'x
                                     (cons 'y nil))
                               (cons
                                (cons '+
                                      (cons 'x (cons 'y nil)))
                                nil))))
Returns
(DEFUN SUM (X Y) (+ X Y))

While it is simply a linked list of cons cells, it's useful to think of it another way: a tree. Each cons cell has two parts: a car and a cdr. The car holds a leaf in the tree, whereas the cdr holds either another branch (in the common case) or another leaf. When working with lists as a tree, you can use car and cdr to access those two positions.

(defparameter *my-tree* (cons 'defun (cons 'sum (cons (cons 'x
                                     (cons 'y nil))
                               (cons
                                (cons '+
                                      (cons 'x (cons 'y nil)))
                                nil)))))

Now call car on the tree:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(car *my-tree*)
Returns
DEFUN
Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(cdr *my-tree*)
Returns
(SUM (X Y) (+ X Y))

copy-tree returns a copy of a tree of cons cells:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(copy-tree *my-tree*)
Returns
(DEFUN SUM (X Y) (+ X Y))

subst returns a new list that substitutes leaves in the tree:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(subst 'z 'y *my-tree*)
Returns
(DEFUN SUM (X Z) (+ X Z))

sublis does the same with multiple leaves in the tree:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(sublis '((sum  . subtract)
          (+    . -)
          (x    . a)
          (y    . b))
        *my-tree*)
Returns
(DEFUN SUBTRACT (A B) (- A B))
3.4.4LISP FUNDAMENTALS

Lists as Tables

sublis takes a special type of list for its first argument: an association list, otherwise known as an alist or table. An alist is a list with nested lists.

(defparameter *en-to-ja-table* '((one   . ichi)
                                 (two   . ni)
                                 (three . san)
                                 (four  . yon)
                                 (five  . go)))

With alists, the car is a key and the cdr is a value. You can search a table by either key or value using assoc and rassoc.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(assoc 'two *en-to-ja-table*)
Returns
(TWO . NI)
Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(rassoc 'ni *en-to-ja-table*)
Returns
(TWO . NI)

You can get at the value using cdr.

(cdr (assoc 'two *en-to-ja-table*))
Returns
NI

You can either use a dotted-list–a cons cell that has a non-nil value in the cdr position as in *en-to-ja-table* above–or a regular list.

(defparameter *en-to-ja-table* '((one   ichi)
                                 (two   ni)
                                 (three san)
                                 (four  yon)
                                 (five  go)))

The only difference is that you need to use cadr to get at the value if you don't use dotted-lists.

(cdr (assoc 'two *en-to-ja-table*))
Returns
(NI)
Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(cadr (assoc 'two *en-to-ja-table*))
Returns
NI
3.4.5LISP FUNDAMENTALS

Lists as Sets

Lists can also be treated like sets–an unordered sequence of unique elements.

With adjoin, you can add a single unique element.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(adjoin 'three '(one two three))
Returns
(ONE TWO THREE)

Notice that 'three only occurs once. Since it was already in the "set", it wasn't added.

(adjoin 'four '(one two three))
Returns
(ONE TWO THREE FOUR)

Contents of two sets can be compared to form new sets:

(defparameter *pizza* '(salty sweet cheese sauce round carbs))
(defparameter *cake* '(sweet chocolate brown carbs round))

intersection returns a set of unique elements that are present in both *pizza* and *cake*:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(intersection *pizza* *cake*)
Returns
(CARBS ROUND SWEET)

union returns a set that combines all unique elements of both *pizza* and *cake*:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(union *cake* *pizza*)
Returns
(SAUCE CHEESE SALTY SWEET CHOCOLATE BROWN CARBS ROUND)

set-difference returns a set that includes all of the elements in *cake* that are not present in *pizza*:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(set-difference *cake* *pizza*)
Returns
(BROWN CHOCOLATE)

Or vice versa:

(set-difference *pizza* *cake*)
Returns
(SAUCE CHEESE SALTY)

subsetp returns t if a set is a subset of some other set or nil otherwise:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(defparameter *cheese-pizza* '(salty cheese sauce round carbs))
(subsetp *cheese-pizza* *pizza*)
Returns
T
3.4.6LISP FUNDAMENTALS

Mutating Places

In the section on variables, setf was used above to modify the value of a variable defined with defvar. While modifying variable values is a typical use, setf is used for modifying places in many other situations.

For example, it can be used to modify the value of an item in an alist. Say we have an alist like this:

(defvar *some-alist* (list (list 'a 1) (list 'b 2) (list 'c 3)))
(second (assoc 'c *some-alist*))
Returns
3

We can modify the value associated with the c key using setf:

(setf (second (assoc 'c *some-alist*)) 7)
Returns
7

Now let's check the value of c again.

(second (assoc 'c *some-alist*))
Returns
7

In BEYOND LISTS, we will see how to use setf to modify the values of items in other kinds of objects beyond lists.

3.5LISP FUNDAMENTALS

CONTROL FLOW

Common Lisp contains a number of built-in functions for comparisons, judging equality, logical operations, and conditions. Unlike other languages, Common Lisp doesn't really have some kind of universal comparison operator like ==. It can be confusing at first, but after a while, you'll get the hang of it.

To start with, we need to know how truth and falsehood is represented.

t is true and nil is false. In the type hierarchy, all data types extend t. nil is also the empty list. Every value except for nil is treated as true.

Common Lisp has the typical comparison operators for math (with /= being non-typical in other languages):

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(= 1 1)
(/= 1 1)
(> 199 180)
(>= 155 155)
(< 7 19)
(<= 77 77)
3.5.1LISP FUNDAMENTALS

General Equality

For symbols, variables, lists, and other objects, you need to use one of eq, eql, equal, or equalp. Each of them tests equality of different degrees. If you come from Python or JavaScript you'll usually expect functionality similar to equal or equalp.

eq will test equality of identity. Do these two objects share the same place in memory?

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(defparameter *deez-nums* '(1 2 3 4 5))
(defparameter *your-nums* '(1 2 3 4 5))
(defparameter *gods-nums* '(one two three four five))

(eq *deez-nums* *your-nums*)                ; NIL, two different lists.
(eq *deez-nums* *deez-nums*)                ; T, same list.
(eq *gods-nums* '(one two three four five)) ; NIL
(eq 'one 'one)                              ; T, symbols are reused when they
                                            ; are from the same package.
(eq '(1 2 3 4 5) '(1 2 3 4 5))              ; NIL, two lists are constructed
                                            ; separately.
(eq #\a #\a)                                ; T
(eq #\a #\A)                                ; NIL
(eq "hello" "hello")                        ; NIL, strings are arrays of
                                            ; characters and are constructed
                                            ; separately.
(defparameter *greeting* "Hello, world!")
(eq *greeting* *greeting*)                  ; T, same array of characters.

eql is the same as eq, except that if the arguments are characters or numbers of the same type then their values (not their places in memory) are compared.

equal tests structural similarity.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(equal '(1 2 3) '(1 2 3))               ; T
(equal '(1 3 2) '(1 2 3))               ; NIL
(equal "hello" "hello")                 ; T
(equal "HELLO" "hello")                 ; NIL
(equal 1 1.0)                           ; NIL, different types

equalp is further lenient:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(equalp #\a #\A)                        ; T
(equalp "hello" "HELLO")                ; T, good for case-insensitive testing
                                        ; of characters or strings
(equalp 1 1.0)                          ; T, good for testing numbers across
                                        ; number types
(equalp '(1 2 3) '(1 3 2))              ; NIL

Characters and strings have their own equality operators: char-equal and string-equal.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(char-equal #\a #\A)                    ; T, same as (equalp #\a #\A)
(string-equal "hello" "HELLO")          ; T, same as (equalp "hello" "HELLO")

Additionally, there are tests like char-greaterp, string=, char<, etc.

For more detail you should look at the Hyperspec.

3.5.2LISP FUNDAMENTALS

Logical Operators

Common Lisp has the typical logical operators.

and tests if two or more forms are true. All forms are evaluated left-to-right, and evaluation stops if any inner forms return nil. The and form returns the value returned by the last form inside it if all forms in it evaluate to non-nil values.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(and 't 5)          ; => 5
(and 't 5 'hello)   ; => HELLO
(and 'nil 5 'hello) ; => NIL

or returns the value of the first form that evaluates to true. If no form passed to it returns a true value, it returns nil.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(or 5 't 'nil 'hello)                  ; => 5
(or 'nil (> 1 5) (eq "hello" "hello")) ; => NIL

or will stop evaluating forms on the first form that evaluates to true.

(or 'nil (> 5 10) (= 1 2) ; all evaluate to nil
    (print "Evaluated, returns true.")
    (print "Not evaluated."))
Returns
Evaluated, returns true.

not will return t if the inner form returns nil, and nil if that form returns t.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(not (= 1 1))  ; => NIL
(not (oddp 2)) ; => T
3.5.3LISP FUNDAMENTALS

Conditional Forms

There are a number of different conditional forms. Of course, there is trusty ol' if. if is a special operator. It takes a test argument. If the test returns true, the next form is evaluated. If the test returns false, then the form after that is evaluated.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(if (> 10 1)                   ; if
    '10-is-greater-than-1      ; then branch
    '10-is-not-greater-than-1) ; (optional) else branch

If you don't need a second (else) branch, you can use when or unless:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(let ((num 0))
  (when (<= 1 num 10)
    (format t "~&FROM WHEN EXPRESSION: ~a is between 1 and 10" num)
    (print num)
    (print (+ num num)))

  (unless (<= 1 num 10)
    (format t "~&FROM UNLESS EXPRESSION: ~a is not between 1 and 10" num)
    (print num)
    (print (+ num num))))

(unless test ...) is equivalent to (when (not test) ...).

One of the upsides of not having an else branch is that you can do multiple operations after the test with when and unless.

cond takes lists of tests and forms to evaluate if the test returns t. The parentheses can be tricky here.

(cond (test form-to-evaluate-if-test-returns-t)
      (test form-to-evaluate-if-test-returns-t))

Here is a practical example:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(defparameter *monster-happiness-meter* 49)
(let ((mhm *monster-happiness-meter*))
  (cond
    ((>= 0 mhm)      (format t "~&Get this monster lifting weights at the gym, now!")    'take-monster-to-gym)
    ((<= 70 mhm 89)  (format t "~&This monster is pretty happy.")                        'hang-out-with-monster)
    ((<= 50 mhm 69)  (format t "~&This monster is feeling a little down.")               'invite-monster-to-lunch)
    ((<= 30 mhm 49)  (format t "~&Get this monster's mommy on the phone.")               'call-monsters-mommy)
    ((<= 1 mhm 29)   (format t "~&Did this monster's grandma die or something?")         'console-monster)
    (t               (format t "~&This is the catchall fallback expression.")            'fallback)))
Returns
CALL-MONSTERS-MOMMY

cond will stop evaluating on the first form that returns t.

case takes a key expression and then some clauses. It evaluates the clauses in order. If the value of the first item of a clause is eql to the value returned by the key expression, the rest of the items in the clause are evaluated.

case is part of the family of case forms: case, ccase, ecase, typecase, ctypecase, and etypecase. If you know case statements from other languages, you understand the basic idea.

case and typecase return nil if no match is found. If you want to trigger errors when no match is found (rather than providing an fallback clause), use ecase and etypecase. If you want the option to provide a value for the key expression and continue the program, use ccase and ctypecase.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(defparameter *env* :DEVELOPMENT)

(defun check-environment ()
  (case *env*
    (:DEVELOPMENT "logging EVERYTHING")
    (:PRODUCTION "locking in and locking down")))

(check-environment)         ; => "logging EVERYTHING"

(let ((*env* :PRODUCTION))
  (check-environment))      ; => "locking in and locking down"

(let ((*env* :AWS))
  (check-environment))      ; => NIL

case will return nil when none of the other cases match. You can set the fallback case using t or otherwise.

(defun check-environment ()
  (case *env*
    (:DEVELOPMENT "logging EVERYTHING")
    (:PRODUCTION "locking in and locking down")
    (otherwise "you gotta set your *env* to :DEVELOPMENT or :PRODUCTION")))

(let ((*env* :AWS))
  (check-environment))
Returns
you gotta set your *env* to :DEVELOPMENT or :PRODUCTION

If you want to return an error if the argument falls through all the checks, use ecase.

(defun check-environment ()
  (ecase *env*
    (:DEVELOPMENT "logging EVERYTHING")
    (:PRODUCTION "locking in and locking down")))

(let ((*env* :AWS))
  (check-environment))
Returns
:AWS fell through ECASE expression.
Wanted one of (:DEVELOPMENT :PRODUCTION).
[Condition of type SB-KERNEL:CASE-FAILURE]

Restarts:
0: [RETRY] Retry SLY evaluation request.
1: [*ABORT] Return to SLY's top level.
2: [ABORT] abort thread (#<THREAD tid=11923 "slynk-worker" RUNNING {70071BE463}>)

If you want a continuable error, use ccase.

(defun check-environment ()
  (ccase *env*
    (:DEVELOPMENT "logging all errors")
    (:PRODUCTION "locking in and locking down")))

(let ((*env* :AWS))
  (check-environment))
Returns
:AWS fell through CCASE expression.
Wanted one of (:DEVELOPMENT :PRODUCTION).
[Condition of type SB-KERNEL:CASE-FAILURE]

Restarts:
0: [STORE-VALUE] Supply a new value for *ENV*.
1: [RETRY] Retry SLY evaluation request.
2: [*ABORT] Return to SLY's top level.
3: [ABORT] abort thread (#<THREAD tid=11955 "slynk-worker" RUNNING {7007B83833}>)

Notice that now you can store a value in *env*. If you do, it will retry the case check with that value.

The typecase family works the same, but will check the type of value returned by the key expression.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(let ((x 5))
  (typecase x
    (list 'this-is-a-list)
    (number 'this-is-a-number)
    (function 'this-is-a-function)
    (otherwise 'i-dont-know-what-this-is)))
Returns
THIS-IS-A-NUMBER

handler-case is another member of the case family that works on conditions and errors. We'll look at it more in the chapter on Errors & Conditions.

3.6LISP FUNDAMENTALS

ITERATION

There are many ways to iterate in Common Lisp. There are the map/filter/reduce operations as in other functional programming languages, but Lisp additionally has do iterators, a special iterator macro, and predicate iterators.

3.6.1LISP FUNDAMENTALS

Iterating by Mapping

You can iterate by using a functional programming style mapping. You've seen it in action already:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(mapcar #'square-then-double '(1 2 3 4 5))
(mapcar #'first '((1 2 3 4 5) (one two three four five) (what is going on here) (once
 upon a time)))
Returns
(1 ONE WHAT ONCE)

There are several varieties of mapping functions. mapcar is the most common. The most general mapping function is map.

;;; Go through each character in the string and upper case it. Return a string.
(map 'string #'char-upcase "hello")
Returns
"HELLO"
;;; Take an item from each of the lists and multiply them, returning a list.
;;; Finishes at the end of the shortest list.
(map 'list #'* '(2 4 6 8) '(3 5 7 9 11 13 15) '(10 10 10))
Returns
(60 200 420)
3.6.2LISP FUNDAMENTALS

Iterating by Reducing

You can reduce multiple values down to one value by applying some function to successive items in the sequence with reduce.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(reduce #'+ '(2 2 2))
Returns
6
(reduce #'* '(2 2 2))
Returns
8
(reduce #'append '((2 2 2) (3 3 3) (4 4)))
Returns
(2 2 2 3 3 3 4 4)

If you define a function to use for reducing, it needs to take two arguments:

(defun sum (x y) (+ x y))

(reduce #'sum '(1 1 1 3 4 5 6 7))
Returns
28
3.6.3LISP FUNDAMENTALS

Iterating by Filtering

You can filter the contents of a sequence with remove-if-not:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(remove-if-not #'oddp '(1 2 3 4 5 6 7 8 9 10))
Returns
(1 3 5 7 9)

You can define your own predicate and pass it to remove-if-not:

(defparameter *dangerous-animals* '(lion tiger bear snake shark))

(defun safe-animal-p (animal)
  (not (member animal *dangerous-animals*)))
(remove-if-not #'safe-animal-p '(dog cat monkey lion hamster shark snake bear
 koala frog))
Returns
(DOG CAT MONKEY HAMSTER KOALA FROG)
(defun greater-than-50-p (num)
  (> num 50))
(remove-if-not #'greater-than-50-p '(40 50 30 90 80 10 70 100 25 60 55 2))
Returns
(90 80 70 100 60 55)

You can also use remove-if:

(defun dangerous-animal-p (animal)
  (member animal *dangerous-animals*))
Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(remove-if #'dangerous-animal-p '(dog cat monkey lion hamster shark snake bear
 koala frog))
Returns
(DOG CAT MONKEY HAMSTER KOALA FROG)
3.6.4LISP FUNDAMENTALS

Iterating by Doing

There are several iteration forms in Common Lisp that involve "doing". Among them, the most popular are dotimes and dolist.

dotimes is for doing something a set number of times.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(dotimes (i 5)
  (if (= i 4)
      (print "I'M NOT CRAZY!!!")
      (print "I'm not crazy!")))
Returns
"I'm not crazy!" 
"I'm not crazy!" 
"I'm not crazy!" 
"I'm not crazy!" 
"I'M NOT CRAZY!!!"  => NIL

dolist will simply iterate through a list. If you're doing simple stuff with lists, this is probably what you want.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(dolist (i '(yet another beautiful short list))
  (format t "~&~a" i))
Returns
YET
ANOTHER
BEAUTIFUL
SHORT
LIST => NIL

dotimes and dolist also take an optional result-form argument. The result-form will be returned at the end of the iteration.

(dotimes (i 5 (format t "~%Finished."))
  (print i))
Returns
0 
1 
2 
3 
4 
Finished. => NIL

The most common use of the result-form is to return data that is collected/transformed during the iteration.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(defun my-reverse (list)
  (let ((result nil))
    (dolist (item list result)
      (push item result))))

(my-reverse '(this list will be returned in reverse order))
Returns
(ORDER REVERSE IN RETURNED BE WILL LIST THIS)

Another common use is to return t

(defun all-even-p (nums)
  "Return T if all NUMS are even, NIL if any are odd."
  (dolist (i nums t)
    (format t "~&Looking at ~a..." i)
    (when (oddp i)
      (format t "~&Oops, this is odd!")
      (return nil))))

(all-even-p '(1 2 3 4 5))
Returns
Looking at 1...
Oops, this is odd! => NIL
(all-even-p '(2 4 6))
Returns
Looking at 2...
Looking at 4...
Looking at 6... => T

It's not strictly necessary to have a result-form. You can just manually return it.

(defun my-reverse (list)
  (let ((result nil))
    (dolist (item list result)
      (push item result))))

(defun my-reverse-manual-return (list)
  (let ((result nil))
    (dolist (item list)                 ; result not set as result-form
      (push item result))
    result))                            ; reversed manually called at the end of
                                        ; the let block

Notice the use of push here. It is a destructive function that modify lists by adding items. push puts items at the front of the list, returning the modified list.

The corresponding pop function removes the first item and returns that item.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(let ((my-list '(1 2 3 4 5))
      (result-list nil))
  (dolist (item my-list (list my-list result-list))
    (push (pop my-list) result-list)))
Returns
(NIL (5 4 3 2 1))

do is the most general and powerful of the do family. It is also a bit complicated and difficult to read/understand.

(do ((var1 init1 [update1])
     (var2 init2 [update2])
     ...)
    (test action-1... action-n) ; base case
 body)
Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(defun all-even-do-p (nums)
  (do ((n nums (cdr n)))
      ((null n) (return t))
    (format t "~&Looking at ~a..." (first n))
    (when (oddp (first n))
      (format t "~&~a is odd." (first n))
      (return nil))))
(all-even-do-p '(2 4 6 7 8 10))
Returns
Looking at 2...
Looking at 4...
Looking at 6...
Looking at 7...
7 is odd. => NIL

Unlike dotimes and dolist, which take care of incrementing the counter or stepping through the list, do requires you to specify the step/update at the end of each iteration. It also requires you to take care of specifying the base case–the conditions for ending the loop.

do is useful especially for people who are comfortable with iterating recursively because recursion also requires the programmer to specify the stepping function and base case.

(defun check-all-even-recursive (nums)
  (cond ((null nums) t)
        ((oddp (car nums)) (format t "~&~a is odd!" (first nums)) nil)
        (t (format t "~&~a is even." (car nums))
           (check-all-even-recursive (cdr nums)))))
(check-all-even-recursive '(2 4 6 7 8 10))

The step function is (cdr nums). Using cdr to step through a list is called "cdring down" the list. The similarities between the recursive method and the do method make it both powerful and often unergonomic. As a reader of code, when you see either a recursive or do iteration, you have to check the step function and base case–something you don't have to do with dotimes, dolist, mapcar, remove-if-not, etc.

The loop iterator is much more widely used than the do iterators (although for small tasks dotimes and dolist are still common), so if you wish for most other Lispers to understand your code and collaborate with others, you should generally prefer loop.

If you prefer a more applicative/functional style, then the map family, remove-if and remove-if-not, and reduce are the way to go.

3.6.5LISP FUNDAMENTALS

Iterating by Looping

The most popular iterating construct in Common Lisp is the loop macro:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(loop :for item :in '(one two three)
      :do (print item))

loop is a macro that uses its own syntax. If you come from Python or JavaScript, it doesn't look so strange, but it looks different from typical Lisp code.

(loop :for n :in '(1 2 3 4 5)
      :collect (* 2 (sqrt n)))
Returns
(2.0 2.828427 3.4641016 4.0 4.472136)

The loop macro will get a fuller treatment in a later chapter.

3.6.6LISP FUNDAMENTALS

Predicate Iterators

If you only need to test the sequence, check-all-even could be rewritten using every.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(every #'evenp '(2 4 6 8 10))
                                        ; => T
(every #'evenp '(1 4 6 8 10))
                                        ; => NIL

every runs a test on all items of a sequence and returns t if the test returns t for every item.

Similarly, some tests all items of a sequence, but will return t as soon as the test returns t for an item.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(some #'oddp '(2 4 6 8))
                                        ; => NIL
(some #'oddp '(1 4 6 8))
                                        ; => T

notevery runs a test on all items of a sequence and returns t if the test returns nil for one item.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(notevery #'oddp '(1 3 5))
                                        ; => NIL
(notevery #'oddp '(1 2 3 5))
                                        ; => T

notany runs a test on all items of a sequence and returns t if the test returns nil for every item.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(notany #'oddp '(2 4 6))
                                        ; => T
(notany #'oddp '(1 2 4 6))
                                        ; => NIL
3.6.7LISP FUNDAMENTALS

Early Returns

There are times when it's necessary to do an early return. We saw an example with all-even-p:

(defun all-even-p (nums)
  (dolist (i nums t)
    (format t "~&Looking at ~a..." i)
    (when (oddp i)
      (format t "~&Oops, this is odd!")
      (return nil))))

The result-form set for dolist is t, meaning that when we get to the end of the list we should return t.

However, when we spot an odd number, we need to return from the dolist loop early using (return nil).

3.7LISP FUNDAMENTALS

STRINGS & I/O

You can print information in the REPL using print.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(print "hello world")

print both sends data to the REPL, but also returns the data as a value. That means you can place print over many different kinds of code, making it useful for simple debugging.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(defun factorial (x)
  (labels ((%factorial (n)
             (cond ((< n 0) nil)
                   ((= n 0) 1)
                   (t (print (* n (%factorial (- n 1))))))))
    (%factorial x)))
(factorial 5)
Returns
1 2 6 24 120 => 120
3.7.1LISP FUNDAMENTALS

Using Sequence Operations on Strings

Strings in Common Lisp are vectors of characters. As a result, all operations that can be used on sequences can be used on strings or vectors (explained in a later chapter).

concatenate combines two or more sequences–meaning it can combine lists, vectors, or strings. The first argument specifies the output type.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(concatenate 'list '(#\h #\e #\l #\l #\o #\space) '(#\w #\o #\r #\l #\d))
Returns
(#\\h #\\e #\\l #\\l #\\o #\\Space  #\\w #\\o #\\r #\\l #\\d)
(concatenate 'vector '(#\h #\e #\l #\l #\o #\space) '(#\w #\o #\r #\l #\d))
Returns
#(#\h #\e #\l #\l #\o #\Space  #\w #\o #\r #\l #\d)
(concatenate 'string '(#\h #\e #\l #\l #\o #\space) '(#\w #\o #\r #\l #\d))
Returns
"hello world"
(concatenate 'string "hello " "world")
Returns
"hello world"

length can not only tell you the number of items in a list, it can also tell you the number of characters in a string.

(length "hello world")
Returns
11

reverse can place characters in a string in reverse order.

(reverse "YOU WILL REWRITE THAT THING IN COMMON LISP IMMEDIATELY")
Returns
"YLETAIDEMMI PSIL NOMMOC NI GNIHT TAHT ETIRWER LLIW UOY"

map is the most general of the mapping functions. If you pass string as the result type, you can run character operations on the string and get back a new string.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(map 'string #'char-upcase "hello")
Returns
"HELLO"
3.7.2LISP FUNDAMENTALS

String Specific Operations

There are also functions specialized to work on strings.

The previous map call can be simplified using string-upcase.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(string-upcase "almighty")
Returns
"ALMIGHTY"

You can probably guess what string-downcase does.

string= and string-equal can be used to test if two strings are the same. string= is case-sensitive, string-equal is case-insensitive.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(string= "almighty" "almighty")         ; => T
(string= "almighty" "Almighty")         ; case-sensitive => NIL
(string-equal "almighty" "ALMIGHTY")    ; case-insensitive => T

There are other string comparison operators: string>, string/=, string-not-greaterp, etc.

3.7.3LISP FUNDAMENTALS

Writing & Reading Files

Streams are either a source or destination for some data. Common Lisp uses streams for reading and writing files, etc.

Use with-open-file to create a block where a streaming connection with some file is active.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(with-open-file (stream #P"io-test.txt" :direction          :output
                                        :if-exists          :supersede
                                        :if-does-not-exist  :create)
  (format stream "~&Put this in the test file.~&This will be on a new line.~&"))

with-open-file is a macro providing a shortcut to using the lower-level functions open and close combined with unwind-protect. It ensures that the connection to the file is closed before leaving the block.

An exhaustive explanation of all the options open and with-open-file can take are in the Hyperspec.

If you want to read a file, set :direction to :input and use one of read, read-line, or read-char.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(with-open-file (stream #P"io-test.txt" :direction :input)
  (loop for line = (read-line stream nil nil)
        while line
        do (format t "~a~&" line)))
3.7.4LISP FUNDAMENTALS

Beyond the Basics w/Files

Common Lisp functions for working with file systems, paths, etc. are generally not portable between Lisp implementations. The package UIOP is the defacto-standard source of portable path and filesystem utilities. Even with UIOP, pathnames can still be tricky.

3.7.5LISP FUNDAMENTALS

Formatting

format is used to write strings to output streams. The first argument is the stream. If set to t, then it will send the input to *standard-output*, which is the output stream to the REPL.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(format t "~&Almighty Lisp~%")

If the first argument is set to nil, then it returns a string.

(format nil "~&Almighty Lisp~%")

However, as in the with-open-file examples, you can also use format to write to file streams, etc.

format has an extensive set of control-string directives used for customizing how text is formatted. All of the directives begin with tilde, such as ~a, ~&, etc. They also have an extensive set of modifiers. Complex format strings are vaguely similar to regular expressions and tend to get just as hairy.

I will cover the bare minimum here. Refer to the Hyperspec to learn all of the directives. I will explain any other directives as necessary.

Tilde a will output the data in human readable, "Aesthetic" format.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

(format t "~a" (aref "hello" 0))
Returns
h => NIL NOTE: not #\h

format takes an arbitrary number of arguments after the format string. For each argument, you need to supply another Tilde a or some other directive.

Tilde % and Tilde & output newlines. Tilde % will always output a newline. Tilde & will only output a newline if the output stream is not on a newline already.

(defparameter *alist* '((:micah male 40 married japan)
                        (:takae female 35 married japan)
                        (:mom female 71 married america)
                        (:papa male 75 married america)))
(loop for row in *alist*
      do (format t "~&~a: ~a~%" (first row) (rest row)))
Returns
MICAH: (MALE 40 MARRIED JAPAN)
TAKAE: (FEMALE 35 MARRIED JAPAN)
MOM: (FEMALE 71 MARRIED AMERICA)
PAPA: (MALE 75 MARRIED AMERICA)
 => NIL
3.8LISP FUNDAMENTALS

PUTTING IT ALL TOGETHER: TIC-TAC-TOE

I think some typing exercise is in order. We'll do a tiny little project.

For this project we'll be making a game of tic-tac-toe complete with a computer opponent. Tic-tac-toe is a relatively common coding project. The version we'll be making is by David Touretzky.

The purpose of this project is to help you get a feel for what editing code is like in Emacs. You'll have a chance to try out structural editing (if you want), and you'll get some practice with tricky forms like let and cond that use an abundance of parentheses.

Common Lisp code tends to be wider than other languages. This is partially because of the culture of using unabbreviated names, but also because of Lisp's functional programming roots. As a result, Lisp programmers often make smaller abstractions to make code more readable and fit into the line-width limits of the editor. This project will give you some exposure to both the "problem" of wider code and the solutions for it.

In order to get the most out of this project, you should follow along and actually type out the code in Emacs, rather than copying and pasting.

3.8.1LISP FUNDAMENTALS

Choose data representation

Tic-tac-toe works like this:

  • There are two players.
  • Each take turns putting their "piece" on the board–either an X or an O.
  • If either player gets three of their pieces in a row vertically, horizontally, or diagonally, they win.
  • If all spaces are filled without any player winning, it's a draw.

The game is simple, so the data representation can be simple.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defvar *board*)

(defun reset-board ()
  (setf *board* (list 'board
                      0 0 0
                      0 0 0
                      0 0 0)))

(defparameter *player-one* 1)
(defparameter *player-two* 10)

The board is a flat list of 0's representing empty spaces. board is a filler symbol to make later code more intuitive to understand. We use setf to assign the globally scoped *board* variable to the value '(board 0 0 0 0 0 0 0 0 0).

We represent player "pieces" as 1 and 10.

  • Type the above code into a buffer named tic-tac-toe.lisp.
  • Compile each form individually with Sly->Compilation->Compile Defun
  • Without typing anything more, practice evaluating the symbols *player-one* and *player-two* with Sly->Evaluation->Eval Defun.
  • Type (reset-board) in the buffer and then evaluate that form. What is the value of *board*?
3.8.2LISP FUNDAMENTALS

Write functions for manipulating data

Now that we have our data representations settled, we need a way to manipulate it.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defun place-piece (board piece position)
  (setf (nth position board) piece)
  board)

Using setf with nth here is similar to doing something like var[n] = my-value in other languages. You setf to a place, such as a variable, a hashtable key, list position, array index, etc.

Here we assign the place in our *board* to the value of piece. The last value returned by the last form evaluated in a function becomes that function's return value. We return the board to be able to show the updated board to the players.

We also need a way to map positions on the board to all possible winning positions–Touretzky calls them triplets.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defparameter *triplets* '((1 2 3) (4 5 6) (7 8 9) 
                           (1 4 7) (2 5 8) (3 6 9) 
                           (1 5 9) (3 5 7)))       

The first three triplets are horizontal winning positions, the second three are vertical, and the last two are diagonal.

3.8.3LISP FUNDAMENTALS

Write game logic

Next, we need a way to calculate the state of those triplets.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defun sum-triplet (board triplet)
  (+ (nth (first triplet) board)
     (nth (second triplet) board)
     (nth (third triplet) board)))

(defun compute-sums (board)
  (mapcar (lambda (triplet) (sum-triplet board triplet)) *triplets*))

mapcar is one of the many built-in functions that takes a function as an argument. mapcar will apply the function to each item in the sequence and collect them into a list, returning the list.

Let's test it out by placing some pieces manually.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(reset-board)
(place-piece *board* *player-one* 1)
(place-piece *board* *player-two* 2)
(place-piece *board* *player-one* 5)
(place-piece *board* *player-two* 9)
(place-piece *board* *player-one* 7)
(place-piece *board* *player-two* 3)
(place-piece *board* *player-one* 4)
Returns
(BOARD 1 10 10 1 1 0 1 0 10)

Now let's test compute-sums.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(compute-sums *board*)
Returns
(21 2 11 3 11 20 12 12)

We can see now that player one, represented as 1s on the board, occupies all three spaces in a triplet. We have a winner, but our program doesn't know that yet. Let's add win and tie detection.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defun winner-p (board)
  (let ((sums (compute-sums board)))
    (or (member (* 3 *player-one*) sums)
        (member (* 3 *player-two*) sums))))

(defun tie-p (board)
  (not (member 0 board)))

Test them on the current board:

(winner-p *board*)
Returns
(3 11 20 12 12)
(tie-p *board*)
Returns
NIL

member is called a semi-predicate: it searches a sequence like sums for an item like (* 3 *player-one*). If none is found, it returns nil. If one is found, however, it doesn't return t; it returns a list of the item plus the rest of the list after the item.

We can add pieces to the board, calculate the state of the board, and detect a winner. If we make an interface for two human players, we can have a game.

3.8.4LISP FUNDAMENTALS

Representing data to the player

We need a way to show the board to players using format. Instead of trying to do everything at once, we'll break it down into pieces, starting with converting player pieces from numbers to letters

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defun convert-to-letter (piece)
  (ecase piece
    (0  " ")
    (1  "X")
    (10 "O")))

(defun opponent (piece)
  (if (= piece 1)
      10
      1))

Test convert-to-letter:

(convert-to-letter 1)
Returns
X
(convert-to-letter 10)
Returns
O

Now let's print a row from the board.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defun print-row (x y z)
  (format t "~& ~a | ~a | ~a ~%" (convert-to-letter x) (convert-to-letter y) (convert-to-letter
 z)))

Now let's print the entire board.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defun print-board (board)
  (format t "~&")
  (print-row (nth 1 board) (nth 2 board) (nth 3 board))
  (format t "-----------")
  (print-row (nth 4 board) (nth 5 board) (nth 6 board))
  (format t "-----------")
  (print-row (nth 7 board) (nth 8 board) (nth 9 board))
  (format t "~&~%"))
(print-board *board*)
Returns
 X | O | O 
-----------
 X | X |   
-----------
 X |   | O 

=> NIL

Here's where having board as a filler item in the *board* list is useful: the calls to nth here are more intuitive.

3.8.5LISP FUNDAMENTALS

Getting user input

Now we need to get player input. We need to ensure that our input is well-formed and that the move from the player is legal.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defun read-legal-move (piece board)
  (let ((move (read)))
    (cond ((not (integerp move))                                                    
           (format t "~&Moves must be a number between 1-9. Your move: ~a~%" move) 
           (print-board board)                                                   
           (read-legal-move piece board))                                           
          ((not (and (>= move 1) (>= 9 move)))
           (format t "~&Choose a space between 1 and 9.")
           (print-board board)
           (read-legal-move piece board))
          ((/= (nth move board) 0)
           (format t "~&You must place a piece on an empty space.")
           (print-board board)
           (read-legal-move piece board))
          (t move))))

The function read is how we request user input from the REPL. In the cond form–which can look pretty hairy to our new Lisp brothers–we run a few checks. integerp is a predicate function (with names typically ending with p or -p) that checks if the user input is an integer. If the player input isn't a number, we tell them we need a number, print the board, and let the player try again.

The next check makes sure that the number the user inputted was a number between 1 and 9.

Finally, we need to check that the space chosen by the player is empty.

If the move passes all the checks, then the cond will evaluate the final form (t move). This is the conventional way of providing a default branch in the cond if all other conditions return nil. In this instance, we just return move.

Now we can make a move.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defun move (piece board)
  (format t "~&It's ~a's turn.~%" (convert-to-letter piece))
  (print-board board)
  (let* ((move (read-legal-move piece board))
         (updated-board (place-piece board piece move))
         (winner (winner-p updated-board)))
    (cond (winner
            (format t "~a wins!" (convert-to-letter (/ (first winner) 3))))
          ((tie-p updated-board)
            (format t "It's a tie!"))
          (t (move (opponent piece) board)))))

(defun play-game ()
  (reset-board)
  (print "X goes first")
  (move *player-one* *board*))

let* is how we bind locally-scoped variables. let*, unlike the regular let, can bind variables to values that were computed earlier in the form. We pass move to place-piece and bind updated-board to the value returned. If we used let instead, we would get an error.

3.8.6LISP FUNDAMENTALS

Write computer moving logic

At this point, the human-vs-human version of the game is feature-complete. What we want now is to add a computer opponent.

At minimum, the computer needs to do the following:

  • If there is a winning move, choose it.
  • If there is a blocking move, choose it.
  • Otherwise, take a random position.

That's easy to do:

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defun choose-move (board)
  (or (make-three-in-a-row board)    
      (block-opponent-win board)    
      (take-random-position board)))

or will evaluate its arguments in order. It will stop evaluation on the first argument that returns a non-nil value.

The computer needs to know if a winning move is available. There is a winning move available if any of the triplets sum to 20.

First, let's make a test board.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defparameter *test-board* '(board
                             10 1 0
                             10 1 0
                             0 0 0))

What we want to do is find a triplet that sums to 20.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(find-if (lambda (triplet) (= 20 (sum-triplet *test-board* triplet))) ,*triplets*)
Returns
(1 4 7)

find-if takes a predicate function and returns the first value in a sequence that evaluates to t when the predicate is applied to it. It iterates over *triplets*, and tests if any of the triplets on the board sum up to 20.

If we find a triplet with a winning move, we want to return the position on the board to take. That means we need to find the element in the winning triplet that is 0.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(find-if (lambda (element) (= 0 (nth element *test-board*)))
         (find-if (lambda (triplet) (= 20 (sum-triplet *test-board* triplet))) *triplets*))
Returns
7

Since we need to do the same thing to check if we need to block…

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(find-if (lambda (element) (= 0 (nth element *test-board*)))
         ;; Notice (= 2 ...), not (= 20 ...)
         (find-if (lambda (triplet) (= 2 (sum-triplet *test-board* triplet))) *triplets*))
Returns
8

…we can make this one function.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

;;; NOTE: bug left purposefully for teaching purposes
(defun win-or-block (board target-sum)
  (let ((target-triplet (find-if (lambda (triplet) (= target-sum (sum-triplet *test-board* triplet)))
                                 *triplets*)))
    (when target-triplet
        (find-if (lambda (element) (= (nth element board) 0)) target-triplet))))

Test it. Passing 2 checks if we need to block; passing 20 checks if we can win:

(win-or-block *test-board* 2)
Returns
8
(win-or-block *test-board* 20)
Returns
7

Now that we have a function that can find spaces to either win or block a win, we can call it with the appropriate target-sum in our win and block strategies.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defun make-three-in-a-row (board)
  (let ((move (win-or-block board (* 2 *player-two*))))
    (when move
        (list move (format nil "~&I see a winning move at ~a.~%" move)))))

(defun block-opponent-win (board)
  (let ((move (win-or-block board (* 2 *player-one*))))
    (when move
        (list move (format nil "~&Danger! Loss imminent! Moving to block at
 ~a.~%" move)))))
(make-three-in-a-row *test-board*)
Returns
(7 "I see a winning move at 7. ")
(block-opponent-win *test-board*)
Returns
(8 "Danger! Loss imminent! Moving to block at 8. ")

We will return a list with the move and also the strategy employed.

If the computer is going first, it should just take a random position.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defun take-random-position (board)
  (let ((move (+ 1 (random 9))))
    (cond ((= 0 (nth move board))
           (place-piece board *player-two* move)
           (list move "Picking random position."))
          (t
           (take-random-position board)))))

random will choose a semi-random value between 0 and the argument passed. Unfortunately, there is no way to specify the beginning of the "range". Since we need to pick a number between 1 (remember the filler BOARD symbol) and 9 inclusive, we add 1 to the result.

Since the random position chosen may be occupied, the catchall branch simply makes a recursive call to take-random-position to try again.

Right now, move calls itself with the opponent player. We'll need a human-move and computer-move to give us the ability to let the computer choose.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defun human-move (board)
  (format t "~&It's ~a's turn.~%" (convert-to-letter *player-one*))
  (print-board board)
  (let* ((move (read-legal-move *player-one* board))
         (updated-board (place-piece board *player-one* move))
         (winner (winner-p updated-board)))
    (cond (winner
           (print-board updated-board)
           (format t "~a wins!" (convert-to-letter (/ (first winner) 3))))
          ((tie-p updated-board)
           (print-board updated-board)
           (format t "It's a tie!"))
          (t (computer-move board)))))

(defun computer-move (board)
  (format t "~&It's ~a's turn.~%" (convert-to-letter *player-two*))
  (print-board board)
  (let* ((move-and-strategy (choose-move board))
         (move (first move-and-strategy))
         (strategy (second move-and-strategy))
         (updated-board (place-piece board *player-two* move))
         (winner (winner-p updated-board)))
    (format t "~&My move: ~a~%" move)
    (format t "~&My strategy: ~a~%" strategy)
    (cond (winner
           (print-board updated-board)
           (format t "~a wins!" (convert-to-letter (/ (first winner) 3))))
          ((tie-p updated-board)
           (print-board updated-board)
           (format t "It's a tie!"))
          (t (human-move board)))))

(defun play-game-with-computer ()
  (reset-board)
  (if (y-or-n-p "Do you want to go first, human?")
      (human-move *board*)
      (computer-move *board*)))

The y-or-n-p function takes user input like read does, but it only accepts two possible inputs: y or n, meaning yes or no. It evaluates to t for yes and nil for no.

3.8.6.1LISP FUNDAMENTALS

Fixing a bug in the middle of a game

Try playing with the computer. You'll notice that the computer is always going with the make-three-in-a-row strategy, even if you let it go first.

In the win-or-block function, there is a bug: we forgot to remove *test-board* from the first find-if.

Try this: Start a game, let the computer go first. It should tell you "My strategy: I see a winning move at". We expect it to simply pick a random space.

While the game is still running, update and compile the win-or-block function.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defun win-or-block (board target-sum)
  (let ((target-triplet (find-if (lambda (triplet)
                                    ;;; NOTE: *test-board* -> board
                                    (= target-sum (sum-triplet board triplet)))
                                *triplets*)))
    (when target-triplet
        (find-if (lambda (element) (= (nth element board) 0)) target-triplet))))

After compiling, continue the game. Make your move. You should now see the computer choose a random space.

This small interaction demonstrates a big feature of Lisp: We can update code as it is running, without restarting it. Whether we are updating a small tic-tac-toe game, a program for music and visualization generation, or a running web app, we can update it while it's running.

3.8.7LISP FUNDAMENTALS

Add computer strategies

The simple data representation we chose at the beginning has made it fairly easy to get a simple human-vs-computer tic-tac-toe game made. However, the computer is very dumb. It doesn't think ahead and doesn't recognize different human strategies. Let's change that.

Tic-tac-toe has a rather unfun characteristic: if played well by both players, every game will end in a draw.

For our computer strategies, then, we are going to be mostly reacting to the human player, recognizing different strategies and countering them perfectly. By the end, we'll totally drain what little fun can be had from the game. But the coding will be fun, so let's go.

There are two strategies you can employ that can guarantee a victory if the opponent doesn't react correctly: the squeeze play and a two-on-one play.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defparameter *squeeze* (list 'board 1 0 0 0 10 0 0 0 1))
(print-board *squeeze*)
Returns
 X |   |   
-----------
   | O |
-----------
   |   | X 
 => NIL
Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defparameter *two-on-one* (list 'board 1 0 0 0 1 0 0 0 10))
(print-board *two-on-one*)
Returns
 X |   |   
-----------
   | X |
-----------
   |   | O 
 => NIL

The squeeze happens when one player takes two corners and one player takes the middle. The two-on-one happens when one player takes the corner and the middle, and the other player takes a corner as well. In both scenarios, O is guaranteed to lose if X plays properly.

To avoid these two scenarios, the computer must recognize possible strategies being deployed against it and react correctly.

  • If the computer is in the middle between two human pieces, it's a possible squeeze.
    • To counter, take a side: don't take a corner.
  • If the computer is in a corner and the human has the middle and the corner lining up his pieces against the computer, it's a possible two-on-one.
    • To counter, take a corner: don't take a side.

Right now, the computer reads the board as a list of triplets and identifies possible wins and danger. But to ensure both players end the game disappointed, the computer needs to recognize some other characteristics of the board: corners and sides.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defparameter *corners* '(1 3 7 9))
(defparameter *sides* '(1 2 3 4 6 7 8 9))

Let's start by detecting a squeeze.

First, we need to search the board and cross-reference the triplets, looking for any triplet with values that reduce to 12

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defun detect-squeeze (board)
  (find-if (lambda (triplet)
               (= 12 (sum-triplet board triplet)))
           *triplets*))
(detect-squeeze *squeeze*)
Returns
(1 5 9)
(detect-squeeze *two-on-one*)
Returns
(1 5 9)

We need to know for sure this is a diagonal triplet, though.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defun diagonal-p (triplet)
  (every (lambda (item)
             ;; Every item is either a corner or the middle.
             (or (member item *corners*)
                 (= 5 item)))
         triplet))
(defun detect-squeeze (board)
  (find-if (lambda (triplet)
               ;; Add AND and DIAGONAL-P
               (and (= 12 (sum-triplet board triplet))
                    (diagonal-p triplet)))
           *triplets*))

Let's try again:

(detect-squeeze *squeeze*)
Returns
(1 5 9)
(detect-squeeze *two-on-one*)
Returns
(1 5 9)

Both still return the same result.

every runs a predicate function on a sequence and returns t if the predicate evaluated to t for every element of the sequence. In diagonal-p, it checks if every element of the triplet is either a corner or the middle space.

We also need to know who is in the middle.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defun human-in-middle-p (board)
  (= (nth 5 board) *player-one*))

;;; Add TARGET-SUM as an argument.
(defun detect-squeeze (board target-sum)
  (find-if (lambda (triplet)
               ;; Use TARGET-SUM.
               (and (= target-sum (sum-triplet board triplet))
                    (diagonal-p triplet)
                    ;; Add HUMAN-IN-MIDDLE-P.
                    (not (human-in-middle-p board))))
           *triplets*))

Now the two boards produce different results:

(detect-squeeze *squeeze* 12)
Returns
(1 5 9)
(detect-squeeze *two-on-one* 12)
Returns
NIL

Finally, we just need to be sure that we're at the beginning of the game. The player may have already blocked the squeeze or two-on-one, or the game may have otherwise progressed beyond the first diagonals.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defun side-empty-p (board)
  (find-empty-position board *sides*))

(defun find-empty-position (board search-area)
  (find-if (lambda (x) (= 0 (nth x board))) search-area))

(defun detect-squeeze (board target-sum)
  (let ((squeeze-p
          (find-if (lambda (triplet)
                       (and (= (sum-triplet board triplet) target-sum)
                            (diagonal-p triplet)                           
                            (not (human-in-middle-p board))               
                            (side-empty-p board)))                       
                   *triplets*)))
    (when squeeze-p
        (find-empty-position board *sides*))))

If we see a squeeze, we need to counter. To counter a squeeze, we need to take a side (not a corner). So we look for an empty position in one of the *sides*.

If we test detect-squeeze, we should get an empty space on one of the sides:

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(detect-squeeze *squeeze* 12)
Returns
2

2 is the space between corner 1 and 3, so it's given an expected return value.

detect-two-on-one is nearly identical to detect-squeeze:

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defun detect-two-on-one (board target-sum)
  (let ((two-on-one-p
          (find-if (lambda (triplet)
                       (and (= (sum-triplet board triplet) target-sum)
                            (diagonal-p triplet)
                            (human-in-middle-p board) 
                            (side-empty-p board)))
                   *triplets*)))
    (when two-on-one-p
      (find-empty-position board *corners*))))

Test both detection functions against both boards:

(detect-squeeze *two-on-one* 12)
(detect-squeeze *squeeze* 12)
(detect-two-on-one *two-on-one* 12)
(detect-two-on-one *squeeze* 12)
Returns
detect-squeeze on *two-on-one*: NIL
detect-squeeze on *squeeze*: 2
detect-two-on-one on *two-on-one*: 3
detect-two-on-one on *squeeze*: NIL

With that, we just need a couple of small wrappers to encapsulate our strategies.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defun block-squeeze-play (board)
  (let ((move (detect-squeeze board (+ (* *player-one* 2) *player-two*))))
    (when move
      (list move "I'm being squeezed! Taking side space."))))

(defun block-two-on-one-play (board)
  (let ((move (detect-two-on-one board (+ (* *player-one* 2) *player-two*))))
    (when move
      (list move "It's two-on-one! Taking corner space."))))

Finally, we update choose-move by adding our two new strategies.

Buffer

almighty/lisp-fundamentals/tic-tac-toe.lisp

Package

cl-user

(defun choose-move (board)
  (or (make-three-in-a-row board)
      (block-squeeze-play board)         ; Added
      (block-two-on-one-play board)      ; Added
      (block-opponent-win board)
      (take-random-position board)))

Now you have a finished AI that will force a draw every game. Try playing it and enjoy infinite draws.

With this, you have gotten your first experience writing a program in Almighty Common Lisp. It may have been painful: if you aren't using structural editing modes like lispy-mode or paredit-mode, you had to make sure to keep your parentheses balanced and moving code around may have been harder than you expected. However, with practice, you'll be stacking parens like an expert.

3.9LISP FUNDAMENTALS

COMMENTS & FEATURE EXPRESSIONS

In Lisp, you make comments with a semicolon. There are conventions for how many semicolons to use for certain purposes.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

;;;; Four semicolons for the top of a file.
;;; Three semicolons at the top-level, between function calls, etc.
(defun some-function ()
  ;; Two for inside a function, between forms.
  (print "Inside some-function") ; One for commenting to the right of some form.
  (+ 2 2)) 

Lisp also has a way of creating feature flags using feature expressions. Feature expressions are useful for preventing certain forms from being compiled under certain conditions. They look for a feature in the global *features* variable. The expression will act (or not) based on whether it finds the feature in *features*.

For example, let's say you have the following code in a file and try to compile the file:

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

#+windows
(print "This only prints if the code is compiled on a machine running a Windows OS.")

That code will only be compiled if it the machine attempting to compile it is running a Windows OS. You can manually evaluate the form in the buffer (MENU -> SLY -> Compilation -> Compile Defun), but if you try to compile a whole file (MENU -> SLY -> Compilation -> Compile and Load File) it will not compile. This is much more relevant when you are compiling and loading entire systems, which we'll cover in a later chapter.

You can compile some code only if it isn't compiled on a Windows machine.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

#-windows
(print "This will only be compiled on a non-Windows machine when you try to compile the whole file.")

You can use logical operators to check for multiple features.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

#+(not sbcl)
(print "This will only compile if the implementation used to compile the file is not SBCL.")

But again, you can manually evaluate and compile these forms if you want to.

Some people use feature expressions to leave small tests in their code using #+nil.

Buffer

almighty/lisp-fundamentals/main.lisp

Package

cl-user

#+nil
(print "This form will never be compiled with the rest of a file.")

Since nil will never be included in *features*, it's generally safe to leave behind small tests in your code that will only run if you manually place your cursor over them and evaluate them.