Beginner

Operators in Common Lisp

Arithmetic, comparison, logical, and assignment operators in Common Lisp - prefix notation, variadic arguments, and exact rational arithmetic

In most languages, operators are syntactic specials — +, -, *, &&, || all live in a grammar separate from function calls, with precedence and associativity rules baked into the parser. Common Lisp throws that distinction out. Every “operator” is just a function (or macro) called in prefix notation inside an S-expression. There is no operator precedence to memorize, because there is nothing to disambiguate: the parentheses already say exactly which call happens first.

This uniformity has practical consequences. Arithmetic functions are variadic — (+ 1 2 3 4 5) is a single call, not chained pairwise additions. Comparison predicates accept any number of arguments and verify the relation across the whole chain — (< 1 2 3) checks 1 < 2 < 3 in one go. And because Common Lisp preserves exact rational arithmetic, (/ 10 3) returns 10/3 rather than silently truncating or producing a floating-point approximation.

In this tutorial you’ll see arithmetic, comparison, logical, and assignment operators, how Common Lisp handles operator precedence through pure structure, and a few Lisp-specific operators like incf, decf, and the setf generalized assignment.

A Tour of Operators

Create a file named operators.lisp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
;; Operators in Common Lisp - every operator is just a function call

;; --- Arithmetic ---
(format t "Arithmetic:~%")
(format t "  (+ 10 5)     = ~a~%" (+ 10 5))
(format t "  (- 10 5)     = ~a~%" (- 10 5))
(format t "  (* 10 5)     = ~a~%" (* 10 5))
(format t "  (/ 10 5)     = ~a~%" (/ 10 5))
(format t "  (/ 10 3)     = ~a~%" (/ 10 3))      ; exact rational, not 3.333
(format t "  (mod 10 3)   = ~a~%" (mod 10 3))
(format t "  (rem -10 3)  = ~a~%" (rem -10 3))   ; rem and mod differ on negatives
(format t "  (mod -10 3)  = ~a~%" (mod -10 3))
(format t "  (expt 2 10)  = ~a~%" (expt 2 10))
(format t "  (sqrt 16)    = ~a~%" (sqrt 16))
(format t "  (abs -7)     = ~a~%" (abs -7))
(format t "  (1+ 41)      = ~a~%" (1+ 41))       ; increment-by-one shorthand
(format t "  (1- 43)      = ~a~%" (1- 43))       ; decrement-by-one shorthand

;; --- Variadic arithmetic ---
(format t "~%Variadic arithmetic (any number of arguments):~%")
(format t "  (+ 1 2 3 4 5)   = ~a~%" (+ 1 2 3 4 5))
(format t "  (* 1 2 3 4 5)   = ~a~%" (* 1 2 3 4 5))
(format t "  (+)             = ~a~%" (+))         ; identity: 0
(format t "  (*)             = ~a~%" (*))         ; identity: 1
(format t "  (- 100 10 5 2)  = ~a~%" (- 100 10 5 2))

;; --- Comparison (numeric) ---
(format t "~%Comparison:~%")
(format t "  (= 5 5)         = ~a~%" (= 5 5))
(format t "  (/= 5 6)        = ~a~%" (/= 5 6))
(format t "  (< 1 2 3)       = ~a~%" (< 1 2 3))   ; chained: 1 < 2 < 3
(format t "  (< 1 3 2)       = ~a~%" (< 1 3 2))   ; not strictly increasing
(format t "  (> 3 2 1)       = ~a~%" (> 3 2 1))
(format t "  (<= 1 1 2)      = ~a~%" (<= 1 1 2))
(format t "  (zerop 0)       = ~a~%" (zerop 0))
(format t "  (plusp -3)      = ~a~%" (plusp -3))
(format t "  (evenp 4)       = ~a~%" (evenp 4))

;; --- Logical / boolean ---
(format t "~%Logical:~%")
(format t "  (and t t nil)   = ~a~%" (and t t nil))
(format t "  (and 1 2 3)     = ~a~%" (and 1 2 3))   ; returns last truthy value
(format t "  (or nil nil 42) = ~a~%" (or nil nil 42))
(format t "  (or nil nil)    = ~a~%" (or nil nil))
(format t "  (not nil)       = ~a~%" (not nil))
(format t "  (not 0)         = ~a~%" (not 0))       ; only NIL is false; 0 is true!

A few details worth flagging:

  • (/) returns rationals. (/ 10 3) is 10/3, an exact value. Use (float (/ 10 3)) if you want 3.3333334.
  • and and or are short-circuiting and return the actual value that decided the result, not a normalized boolean. (or nil nil 42) evaluates to 42.
  • Only NIL is false. 0, "", and the empty list () (which equals NIL) — but no other value — count as false. (not 0) returns NIL because 0 is truthy.

Assignment and Mutation

Create a file named assignment.lisp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
;; Common Lisp uses defvar/defparameter for top-level bindings
;; and setf as the universal "place-setter" operator.

(defparameter *counter* 0)
(format t "Initial *counter* = ~a~%" *counter*)

;; setf assigns a new value to a place
(setf *counter* 10)
(format t "After (setf *counter* 10): ~a~%" *counter*)

;; incf / decf modify in place by an optional delta
(incf *counter*)        ; default delta is 1
(format t "After (incf *counter*):    ~a~%" *counter*)

(incf *counter* 5)
(format t "After (incf *counter* 5):  ~a~%" *counter*)

(decf *counter* 4)
(format t "After (decf *counter* 4):  ~a~%" *counter*)

;; setf works on any "place" - including array elements and hash keys
(let ((vec (vector 10 20 30)))
  (setf (aref vec 1) 99)
  (format t "Vector after (setf (aref vec 1) 99): ~a~%" vec))

;; Local bindings with let - no mutation needed
(let* ((a 3)
       (b 4)
       (hypotenuse (sqrt (+ (* a a) (* b b)))))
  (format t "Hypotenuse of (~a, ~a) = ~a~%" a b hypotenuse))

setf is Common Lisp’s generalized assignment: any expression that names a storage location (a variable, an array slot, a hash-table entry, a struct field, a CLOS slot) can be the first argument to setf. There is no separate =, []=, or property-set syntax — one operator handles them all.

Precedence — There Is None

Create a file named precedence.lisp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
;; In infix languages: 2 + 3 * 4 = 14 because * binds tighter than +.
;; In Common Lisp, there is no precedence - parentheses ARE the grouping.

(format t "(+ 2 (* 3 4))         = ~a~%" (+ 2 (* 3 4)))   ; "2 + 3 * 4"
(format t "(* (+ 2 3) 4)         = ~a~%" (* (+ 2 3) 4))   ; "(2 + 3) * 4"

;; Comparison chained with logical operators
(let ((x 7))
  (format t "x in [1, 10]?  ~a~%"
          (and (<= 1 x) (<= x 10))))

;; The same idea more compactly - <= is variadic
(let ((x 7))
  (format t "x in [1, 10]?  ~a~%" (<= 1 x 10)))

;; Mixing arithmetic and comparison
(format t "(zerop (mod 100 4))   = ~a~%" (zerop (mod 100 4)))
(format t "(> (* 6 7) 40)        = ~a~%" (> (* 6 7) 40))

Because there is no precedence to memorize, the trade-off is more parentheses. In return, every expression’s evaluation order is unambiguous from the page — what you see is the AST.

Running with Docker

1
2
3
4
5
6
7
# Pull the SBCL Docker image
docker pull clfoundation/sbcl:latest

# Run each example
docker run --rm -v $(pwd):/app -w /app clfoundation/sbcl:latest sbcl --script operators.lisp
docker run --rm -v $(pwd):/app -w /app clfoundation/sbcl:latest sbcl --script assignment.lisp
docker run --rm -v $(pwd):/app -w /app clfoundation/sbcl:latest sbcl --script precedence.lisp

Expected Output

Running operators.lisp:

Arithmetic:
  (+ 10 5)     = 15
  (- 10 5)     = 5
  (* 10 5)     = 50
  (/ 10 5)     = 2
  (/ 10 3)     = 10/3
  (mod 10 3)   = 1
  (rem -10 3)  = -1
  (mod -10 3)  = 2
  (expt 2 10)  = 1024
  (sqrt 16)    = 4.0
  (abs -7)     = 7
  (1+ 41)      = 42
  (1- 43)      = 42

Variadic arithmetic (any number of arguments):
  (+ 1 2 3 4 5)   = 15
  (* 1 2 3 4 5)   = 120
  (+)             = 0
  (*)             = 1
  (- 100 10 5 2)  = 83

Comparison:
  (= 5 5)         = T
  (/= 5 6)        = T
  (< 1 2 3)       = T
  (< 1 3 2)       = NIL
  (> 3 2 1)       = T
  (<= 1 1 2)      = T
  (zerop 0)       = T
  (plusp -3)      = NIL
  (evenp 4)       = T

Logical:
  (and t t nil)   = NIL
  (and 1 2 3)     = 3
  (or nil nil 42) = 42
  (or nil nil)    = NIL
  (not nil)       = T
  (not 0)         = NIL

Running assignment.lisp:

Initial *counter* = 0
After (setf *counter* 10): 10
After (incf *counter*):    11
After (incf *counter* 5):  16
After (decf *counter* 4):  12
Vector after (setf (aref vec 1) 99): #(10 99 30)
Hypotenuse of (3, 4) = 5.0

Running precedence.lisp:

(+ 2 (* 3 4))         = 14
(* (+ 2 3) 4)         = 20
x in [1, 10]?  T
x in [1, 10]?  T
(zerop (mod 100 4))   = T
(> (* 6 7) 40)        = T

Key Concepts

  • Prefix notation is uniform. Every operator is a function call: (op arg1 arg2 ...). There is no infix grammar and no precedence table.
  • Arithmetic operators are variadic. (+ 1 2 3 4 5) is a single call. (+) and (*) return the additive and multiplicative identities (0 and 1).
  • Numeric comparisons chain. (< 1 2 3 4) checks the entire chain 1 < 2 < 3 < 4 and returns T only if it holds throughout.
  • Rational arithmetic is exact. (/ 10 3) returns the rational 10/3. You only get floats when at least one operand is a float, or when you explicitly call float.
  • and and or short-circuit and return values, not booleans. They return the actual value that determined the result — useful for default-fallback patterns like (or user-name "anonymous").
  • Only NIL is false. 0, the empty string, and every other value are truthy. The empty list () equals NIL and so is the lone exception.
  • setf is the universal place-setter. One operator assigns to variables, array elements, hash keys, struct fields, and CLOS slots — no special syntax per kind of place.
  • incf/decf are macros, not operators. Because Lisp macros can rewrite source code, (incf x 5) expands into (setf x (+ x 5)) at compile time — a pattern impossible to express as a normal function.

Running Today

All examples can be run using Docker:

docker pull clfoundation/sbcl:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining