Introduction to Common Lisp

Andrew J. Pounds, Ph.D.

🎯 Learning Objectives

After completing this exercise, you will be able to:

1 What is Lisp?

Lisp (LISt Processor) is the second-oldest high-level programming language still in widespread use today, created by John McCarthy at MIT in 1958. Originally designed for artificial intelligence research, Lisp pioneered many programming concepts we now take for granted, including tree data structures, automatic storage management (garbage collection), dynamic typing, and the read-eval-print loop (REPL).

Common Lisp, standardized in 1984 and updated in 1994 (ANSI Common Lisp), is the modern dialect we will use. It supports multiple programming paradigms including:

Lisp source files typically use the .lisp or .lsp extension. We will use Steel Bank Common Lisp (SBCL), a high-performance Common Lisp compiler.

Lisp's most distinctive feature is homoiconicity—code and data share the same structure (lists). This means Lisp programs can easily manipulate other Lisp programs, enabling powerful macro systems.

2 Your First Lisp Program

Create a file called hello.lisp with the following content:

; My first Lisp program
(format t "Hello, World!~%")

Running with SBCL

Open a terminal and navigate to the directory containing your file. Run the program with:

sbcl --script hello.lisp

You should see Hello, World! printed to the console.

You can also use SBCL interactively by simply typing sbcl to enter the REPL (Read-Eval-Print Loop), where you can type and evaluate expressions directly.

Understanding the Code

Code Element Explanation
; Single-line comment (everything after is ignored)
( ) Parentheses delimit all expressions in Lisp
format The formatted output function
t Output destination: t means standard output (terminal)
"Hello, World!~%" Format string; ~% is the newline directive
In Lisp, everything is an expression enclosed in parentheses. The first element is typically a function or operator, and the remaining elements are arguments: (function arg1 arg2 ...)

3 Basic Lisp Concepts

S-Expressions

All Lisp code is written as S-expressions (symbolic expressions). There are two types:

; Atoms
42              ; a number
"hello"         ; a string
x               ; a symbol
nil             ; the empty list / false value
t               ; the boolean true value

; Lists
(1 2 3)          ; a list of three numbers
(+ 2 3)          ; a list that is also code: adds 2 and 3
(a (b c) d)     ; a nested list

Prefix Notation

Lisp uses prefix notation (also called Polish notation): the operator comes first, followed by its operands.

; Mathematical operations
(+ 2 3)          ; → 5       (same as 2 + 3)
(- 10 4)         ; → 6       (same as 10 - 4)
(* 6 7)          ; → 42      (same as 6 * 7)
(/ 20 4)         ; → 5       (same as 20 / 4)

; Operators can take multiple arguments
(+ 1 2 3 4)      ; → 10      (same as 1 + 2 + 3 + 4)
(* 2 3 4)        ; → 24      (same as 2 * 3 * 4)

; Nested expressions
(+ (* 2 3) (- 10 5))  ; → 11  (same as (2*3) + (10-5))

4 Data Types

Common Lisp has a rich type system. Here are the fundamental types you'll use most often:

Numeric Types

Type Description Examples
integer Whole numbers (arbitrary precision) 42, -17, 1000000000000
ratio Exact fractions 1/3, 22/7, -5/2
float Floating-point numbers 3.14, 2.5e10, -0.001
complex Complex numbers #C(3 4) represents 3+4i
Lisp integers have arbitrary precision—they can be as large as your memory allows! Division of integers produces exact ratios, not truncated results: (/ 5 3) returns 5/3, not 1.

Other Fundamental Types

Type Description Examples
string Sequence of characters "Hello", "Lisp is fun!"
character Single character #\A, #\Space, #\Newline
symbol Named identifier x, my-variable, +
boolean True or false t (true), nil (false)
list Ordered collection '(1 2 3), '(a b c)
In Lisp, nil represents both the empty list () and the boolean false value. Everything else is considered "true" in boolean contexts.

5 Variables

Global Variables with defvar and defparameter

Global variables are conventionally named with asterisks (called "earmuffs"):

; Define global variables
(defvar *name* "Alice")     ; only sets if undefined
(defparameter *pi* 3.14159) ; always sets the value
(defvar *count* 0)
(defvar *enrolled* t)

; Use the variables
(format t "Name: ~a~%" *name*)
(format t "Pi: ~a~%" *pi*)

Local Variables with let

Use let to create local variables with limited scope:

(let ((x 10)
      (y 20)
      (name "Bob"))
  (format t "x = ~a, y = ~a~%" x y)
  (format t "Sum = ~a~%" (+ x y))
  (format t "Name: ~a~%" name))
; x, y, and name are not accessible outside the let

Modifying Variables with setq and setf

(defvar *counter* 0)

(setq *counter* 10)     ; set *counter* to 10
(setq *counter* (+ *counter* 1))  ; increment by 1

; setf is more general and works with any "place"
(setf *counter* 20)     ; same as setq for simple variables

Complete Variable Example

; variables.lisp
(defvar *age* 25)
(defvar *gpa* 3.85)
(defvar *name* "Alice")
(defvar *enrolled* t)

(format t "Name: ~a~%" *name*)
(format t "Age: ~a~%" *age*)
(format t "GPA: ~a~%" *gpa*)
(format t "Enrolled: ~a~%" *enrolled*)

; Using let for local scope
(let ((count 100)
      (pi-approx 3.14159)
      (greeting "Hi"))
  (format t "~%Local variables:~%")
  (format t "Count: ~a~%" count)
  (format t "Pi: ~a~%" pi-approx)
  (format t "Greeting: ~a~%" greeting))

6 Console Output

Common Lisp provides several functions for output:

Function Description
(format t ...) Formatted output (most flexible)
(print x) Prints with newline before and space after (readable form)
(princ x) Prints without extra formatting (human-readable)
(write-line str) Prints string with newline
(terpri) Prints a newline only

The format Function

format is the most powerful output function. The first argument is the destination (t for standard output, nil to return a string).

; output.lisp
(let ((x 42)
      (price 19.99)
      (item "widget"))
  
  ; Basic output
  (format t "The answer is ~a~%" x)
  
  ; Multiple values
  (format t "Item: ~a, Price: $~a~%" item price)
  
  ; Using princ (no newline)
  (princ "Processing")
  (princ "...")
  (format t " Done!~%")
  
  ; Formatted numbers
  (format t "The ~a costs $~,2f~%" item price)
  (format t "Hex: ~x, Padded: ~5,'0d~%" x x))

Common Format Directives

Directive Description Example Output
~a Aesthetic (human-readable) hello
~s Standard (machine-readable, with quotes) "hello"
~d Decimal integer 42
~f Floating-point 3.141593
~,2f Float with 2 decimal places 3.14
~x Hexadecimal 2a
~% Newline (line break)
~~ Literal tilde ~
~5,'0d Zero-padded to 5 digits 00042

7 Console Input

Reading input in Common Lisp uses read for Lisp data or read-line for strings:

; input.lisp

; Reading a string
(format t "Enter your name: ")
(force-output)  ; ensure prompt appears before input
(defvar *name* (read-line))

; Reading a number - method 1: parse from string
(format t "Enter your age: ")
(force-output)
(defvar *age* (parse-integer (read-line)))

; Reading a number - method 2: use read for Lisp data
(format t "Enter your GPA: ")
(force-output)
(defvar *gpa* (read))

(format t "Hello, ~a! Age: ~a, GPA: ~,2f~%" *name* *age* *gpa*)
read evaluates input as Lisp data, so numbers are read directly. However, for strings, users would need to type quotes. Use read-line for string input from users, and parse-integer or read-from-string to convert to numbers.

Understanding Input Functions

Function Description Use Case
(read-line) Reads one line as a string User text input
(read) Reads and parses one Lisp expression Numbers, symbols, lists
(parse-integer str) Converts string to integer Integer from text
(read-from-string str) Parses Lisp expression from string Float from text
(force-output) Flushes output buffer Ensure prompt displays

8 Arithmetic Operators

Basic Arithmetic

Operator Description Example Result
+ Addition (+ 10 3) 13
- Subtraction (- 10 3) 7
* Multiplication (* 10 3) 30
/ Division (/ 10 3) 10/3 (ratio)
mod Modulus (remainder) (mod 10 3) 1
rem Remainder (rem 10 3) 1
expt Exponentiation (expt 2 10) 1024
sqrt Square root (sqrt 16) 4.0
abs Absolute value (abs -5) 5

Division Variants

; division.lisp
(format t "Regular division: ~a~%" (/ 7 2))       ; → 7/2 (ratio)
(format t "Float division: ~a~%" (/ 7.0 2))      ; → 3.5
(format t "Floor division: ~a~%" (floor 7 2))    ; → 3
(format t "Ceiling division: ~a~%" (ceiling 7 2)) ; → 4
(format t "Truncate: ~a~%" (truncate 7 2))      ; → 3
(format t "Round: ~a~%" (round 7 2))            ; → 4

Incrementing and Decrementing

(defvar *count* 5)

(incf *count*)      ; *count* is now 6 (increment by 1)
(incf *count* 3)    ; *count* is now 9 (increment by 3)
(decf *count*)      ; *count* is now 8 (decrement by 1)
(decf *count* 2)    ; *count* is now 6 (decrement by 2)

9 Control Structures

The if Special Form

Basic conditional with one then-clause and one optional else-clause:

; Syntax: (if test then-form else-form)
(if (> x 0)
    (format t "Positive~%")
    (format t "Not positive~%"))

; Without else clause
(if (= x 0)
    (format t "Zero~%"))
if only allows ONE form in the then-branch and ONE in the else-branch. To execute multiple forms, wrap them in progn.
; Multiple statements using progn
(if (> score 90)
    (progn
      (format t "Excellent!~%")
      (format t "Grade: A~%"))
    (format t "Keep trying!~%"))

The when and unless Forms

Cleaner alternatives when you only need one branch:

; when - executes body if test is true
(when (> score 90)
  (format t "Excellent!~%")
  (format t "Grade: A~%"))

; unless - executes body if test is false
(unless (= divisor 0)
  (format t "Result: ~a~%" (/ dividend divisor)))

The cond Form

For multiple conditions (like if-else if-else chains):

; grader.lisp
(format t "Enter your score (0-100): ")
(force-output)
(let ((score (parse-integer (read-line))))
  (cond
    ((>= score 90) (format t "Grade: A~%"))
    ((>= score 80) (format t "Grade: B~%"))
    ((>= score 70) (format t "Grade: C~%"))
    ((>= score 60) (format t "Grade: D~%"))
    (t (format t "Grade: F~%"))))  ; t is the default/else case

Comparison Operators

Operator Meaning Example
= Numeric equality (= x 5)
/= Numeric inequality (/= x 5)
< Less than (< x 5)
> Greater than (> x 5)
<= Less than or equal (<= x 5)
>= Greater than or equal (>= x 5)
equal General equality (works with strings) (equal str "yes")
string= String equality (string= str "yes")

Logical Operators

Operator Meaning Example
and Logical AND (and (> x 0) (< x 100))
or Logical OR (or (= x 0) (= x 1))
not Logical NOT (not found)
; logical.lisp
(format t "Enter your age: ")
(force-output)
(let ((age (parse-integer (read-line))))
  (format t "Do you have a license? (yes/no): ")
  (force-output)
  (let* ((response (read-line))
         (has-license (string= response "yes")))
    (cond
      ((and (>= age 16) has-license)
       (format t "You can drive.~%"))
      ((and (>= age 16) (not has-license))
       (format t "You're old enough but need a license.~%"))
      (t
       (format t "You're too young to drive.~%")))))

The case Form

For comparing a value against multiple constants:

; days.lisp
(format t "Enter a day number (1-7): ")
(force-output)
(let ((day (parse-integer (read-line))))
  (case day
    (1 (format t "Monday~%"))
    (2 (format t "Tuesday~%"))
    (3 (format t "Wednesday~%"))
    (4 (format t "Thursday~%"))
    (5 (format t "Friday~%"))
    ((6 7) (format t "Weekend!~%"))  ; multiple values
    (otherwise (format t "Invalid day number~%"))))
Like D, Common Lisp allows multiple values in a single case clause using a list: ((6 7) ...). Use otherwise (or t) for the default case.

10 Looping Structures

The loop Macro (Simple Form)

The simplest loop repeats forever until return is called:

; simple-loop.lisp
(let ((count 1))
  (loop
    (when (> count 5)
      (return))
    (format t "Count: ~a~%" count)
    (incf count)))

The loop Macro (Extended Form)

The extended loop provides a rich, English-like syntax:

; Counting from 1 to 5
(loop for i from 1 to 5
      do (format t "Count: ~a~%" i))

; Counting down
(format t "~%Countdown:~%")
(loop for i from 10 downto 0
      do (format t "~a~%" i))

; Step by 2
(format t "~%Even numbers:~%")
(loop for i from 2 to 10 by 2
      do (format t "~a~%" i))

; While condition
(let ((x 1))
  (loop while (<= x 5)
        do (format t "x = ~a~%" x)
           (incf x)))

; Collecting results into a list
(loop for i from 1 to 5
      collect (* i i))  ; → (1 4 9 16 25)

The dotimes Macro

Iterate a specific number of times (counter starts at 0):

; dotimes.lisp
(format t "Counting 0 to 4:~%")
(dotimes (i 5)
  (format t "Iteration ~a~%" i))

; With return value
(dotimes (i 5 "Done!")
  (format t "~a " i))  ; prints 0 1 2 3 4, returns "Done!"

The dolist Macro

Iterate over elements of a list:

; dolist.lisp
(dolist (name '("Alice" "Bob" "Charlie"))
  (format t "Hello, ~a!~%" name))

(dolist (n '(1 2 3 4 5))
  (format t "Square of ~a is ~a~%" n (* n n)))

The do Macro

The most general iteration construct:

; Syntax: (do ((var init step) ...) (end-test result) body...)

; Count from 1 to 5
(do ((i 1 (+ i 1)))       ; var, init, step
    ((> i 5))              ; end-test
  (format t "Count: ~a~%" i))

; Multiple variables
(do ((i 0 (+ i 1))
     (sum 0 (+ sum i)))
    ((> i 10) sum)        ; returns sum when done
  (format t "i=~a, sum=~a~%" i sum))

Nested Loops

; multiplication-table.lisp
(format t "Multiplication Table (1-5):~%~%")

; Print header row
(format t "    ")
(loop for col from 1 to 5
      do (format t "~4d" col))
(format t "~%   --------------------~%")

; Print table rows
(loop for row from 1 to 5
      do (format t "~2d |" row)
         (loop for col from 1 to 5
               do (format t "~4d" (* row col)))
         (format t "~%"))

Loop Control: return and return-from

; break-equivalent: return exits the immediately enclosing block
(format t "Finding first multiple of 7:~%")
(loop for i from 1 to 100
      do (when (= (mod i 7) 0)
           (format t "Found: ~a~%" i)
           (return)))

; continue-equivalent: use conditional to skip
(format t "~%Numbers 1-10, skipping multiples of 3:~%")
(loop for i from 1 to 10
      do (unless (= (mod i 3) 0)
           (format t "~a~%" i)))

11 Defining Functions

Functions are defined using defun:

; Define a simple function
(defun square (x)
  "Returns the square of x."  ; optional docstring
  (* x x))

; Call the function
(format t "5 squared = ~a~%" (square 5))

; Function with multiple parameters
(defun greet (name greeting)
  (format t "~a, ~a!~%" greeting name))

(greet "Alice" "Hello")

; Function that returns a value
(defun celsius-to-fahrenheit (c)
  (+ (* c 9/5) 32))

(format t "20°C = ~a°F~%" (celsius-to-fahrenheit 20))

12 Complete Example: Temperature Converter

This program demonstrates input, output, control structures, and loops working together:

; temperature-converter.lisp

(defun celsius-to-fahrenheit (c)
  "Convert Celsius to Fahrenheit."
  (+ (* c 9.0 (/ 1 5.0)) 32.0))

(defun fahrenheit-to-celsius (f)
  "Convert Fahrenheit to Celsius."
  (* (- f 32.0) 5.0 (/ 1 9.0)))

(defun main ()
  (format t "=== Temperature Converter ===~%")
  (format t "1. Celsius to Fahrenheit~%")
  (format t "2. Fahrenheit to Celsius~%")
  (format t "3. Quit~%")
  
  (loop
    (format t "~%Enter choice (1-3): ")
    (force-output)
    (let ((choice (parse-integer (read-line) :junk-allowed t)))
      (case choice
        (1
         (format t "Enter temperature in Celsius: ")
         (force-output)
         (let* ((celsius (read))
                (fahrenheit (celsius-to-fahrenheit celsius)))
           (format t "~,2f°C = ~,2f°F~%" celsius fahrenheit)))
        
        (2
         (format t "Enter temperature in Fahrenheit: ")
         (force-output)
         (let* ((fahrenheit (read))
                (celsius (fahrenheit-to-celsius fahrenheit)))
           (format t "~,2f°F = ~,2f°C~%" fahrenheit celsius)))
        
        (3
         (format t "Goodbye!~%")
         (return))
        
        (otherwise
         (format t "Invalid choice. Please enter 1, 2, or 3.~%"))))))

; Run the program
(main)

📋 Quick Reference

Task Common Lisp Syntax
Print with newline (format t "text ~a~%" variable)
Print without newline (format t "text") or (princ "text")
Formatted print (format t "Value: ~d~%" x)
Read string (read-line)
Read integer (parse-integer (read-line))
Read any number (read)
Define global variable (defvar *name* value)
Define local variable (let ((var value)) ...)
Assign to variable (setq var value)
Define function (defun name (args) body)
Conditional (if test then else)
Multiple conditions (cond (test1 action1) (test2 action2) ...)
Loop n times (dotimes (i n) ...)
Loop with range (loop for i from a to b do ...)
Run program sbcl --script filename.lisp

Programming Exercise: Prime Number Analyzer

Write a Common Lisp program that analyzes prime numbers. Your program should:

  1. Prompt the user to enter a positive integer n (must be ≥ 2)
  2. Validate the input: If the user enters a number less than 2, display an error message and ask again (use a loop)
  3. Determine if n is prime: A prime number is only divisible by 1 and itself
  4. If n is prime: Display "n is a prime number!"
  5. If n is not prime: Display "n is not prime. Its smallest factor is f." (where f is the smallest factor other than 1)
  6. Count and display how many prime numbers exist from 2 up to and including n
  7. Ask the user if they want to check another number (y/n). If yes, repeat; if no, exit with a goodbye message

Sample Output

=== Prime Number Analyzer === Enter a positive integer (>= 2): 1 Invalid input. Please enter a number >= 2. Enter a positive integer (>= 2): 29 29 is a prime number! There are 10 prime numbers from 2 to 29. Check another number? (y/n): y Enter a positive integer (>= 2): 100 100 is not prime. Its smallest factor is 2. There are 25 prime numbers from 2 to 100. Check another number? (y/n): n Goodbye!

Hints

Submission

Save your program as prime-analyzer.lisp and run with:

sbcl --script prime-analyzer.lisp