Intermediate

Functions in Common Lisp

Learn how to define and use functions in Common Lisp - parameters, recursion, closures, and higher-order functions with Docker-ready examples

Functions are the heart of Common Lisp. The language belongs to the Lisp family, where functions are first-class values: they can be stored in variables, passed as arguments, returned from other functions, and created anonymously at runtime. There is no syntactic difference between calling a built-in function and one you define yourself — every expression is a function call written in prefix notation, (function arg1 arg2 ...).

Because Common Lisp is multi-paradigm with a strong functional core, you will rarely write loops where a function and recursion would do. A function’s body is simply a sequence of expressions, and the value of the last one becomes the return value — there is no return keyword needed. This page covers defining functions with defun, the rich parameter system (&optional, &key, &rest), variable scope, recursion, and the higher-order patterns that make functional programming in Lisp so expressive.

By the end you will be able to write reusable functions, pass behavior around as data, and capture state in closures — the building blocks of idiomatic Common Lisp.

Defining and Calling Functions

The defun macro defines a named function. The body’s final expression is automatically returned.

Create a file named functions.lisp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
;; Basic function definition with defun
(defun square (x)
  "Return the square of X."   ; an optional docstring
  (* x x))

;; A function with multiple parameters
(defun add (a b)
  (+ a b))

;; The value of the last expression is returned automatically -
;; there is no 'return' statement in idiomatic Common Lisp
(defun describe-number (n)
  (if (evenp n)
      "even"
      "odd"))

(format t "square of 5 = ~a~%" (square 5))
(format t "add 3 and 4 = ~a~%" (add 3 4))
(format t "7 is ~a~%" (describe-number 7))

A function definition has three parts: the name (square), a parameter list in parentheses ((x)), and a body. The string immediately after the parameter list is an optional documentation string, retrievable at runtime with (documentation 'square 'function).

Optional, Keyword, and Rest Parameters

Common Lisp has one of the most flexible parameter systems of any language. Lambda-list keywords like &optional, &key, and &rest control how arguments are matched.

Create a file named parameters.lisp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
;; &optional parameters can supply a default value
(defun greet (name &optional (greeting "Hello"))
  (format t "~a, ~a!~%" greeting name))

;; &key gives named (keyword) arguments, each with an optional default
(defun make-coffee (&key (size "medium") (shots 1) decaf)
  (format t "~a coffee with ~a shot(s)~a~%"
          size shots (if decaf " (decaf)" "")))

;; &rest gathers any remaining arguments into a list
(defun sum-all (&rest numbers)
  (apply #'+ numbers))

(greet "Ada")
(greet "Ada" "Hi")
(make-coffee)
(make-coffee :size "large" :shots 2)
(make-coffee :decaf t)
(format t "sum-all = ~a~%" (sum-all 1 2 3 4 5))

Keyword arguments are passed by name (:size "large"), so they can appear in any order and you only specify the ones you care about. The &rest parameter collects a variable number of arguments into a list, which apply then spreads back out as arguments to +.

Variable Scope: Local and Global

Common Lisp uses lexical scope by default. let introduces local bindings, and labels defines local functions. Global “special” variables are declared with defparameter and named with *earmuffs* by convention.

Create a file named scope.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
;; A global (dynamic) variable, named with *earmuffs* by convention
(defparameter *bonus* 10)

;; LET introduces local bindings; *bonus* is visible because it is global
(defun pay (base)
  (let ((total (+ base *bonus*)))   ; total is local to PAY
    total))

;; Rebinding a special variable with LET shadows it dynamically
(defun no-bonus-pay (base)
  (let ((*bonus* 0))
    (pay base)))

;; LABELS defines local functions - even mutually recursive ones
(defun countdown (n)
  (labels ((step-down (i)
             (when (>= i 0)
               (format t "~a " i)
               (step-down (1- i)))))
    (step-down n)
    (terpri)))                      ; TERPRI prints a newline

(format t "with bonus: ~a~%" (pay 100))
(format t "no bonus:   ~a~%" (no-bonus-pay 100))
(format t "global:     ~a~%" *bonus*)
(countdown 5)

Note the difference between the two pay calls: no-bonus-pay temporarily rebinds the special variable *bonus* to 0 for the dynamic extent of its let. Once it returns, the global value is unchanged. Local functions defined with labels are only visible inside the enclosing function, keeping helper logic neatly scoped.

Recursion

Recursion is the natural way to iterate in Lisp. A recursive function calls itself with a smaller input until it reaches a base case.

Create a file named recursion.lisp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
;; Classic recursive factorial
(defun factorial (n)
  (if (<= n 1)
      1
      (* n (factorial (1- n)))))

;; Recursion over a list to compute its length
(defun my-length (lst)
  (if (null lst)
      0
      (1+ (my-length (rest lst)))))

;; A function can return more than one value with VALUES
(defun divmod (a b)
  (values (floor a b) (mod a b)))

(format t "10! = ~a~%" (factorial 10))
(format t "length = ~a~%" (my-length '(a b c d)))

;; MULTIPLE-VALUE-BIND receives several return values at once
(multiple-value-bind (q r) (divmod 17 5)
  (format t "17 / 5 = ~a remainder ~a~%" q r))

The factorial function recurses until n reaches 1. my-length walks down a list with rest (the tail) until it hits the empty list nil. The divmod function shows a feature unique to Lisp: a function can return multiple values with values, which the caller destructures using multiple-value-bind.

Higher-Order Functions, Lambda, and Closures

Functions are values. You can create anonymous functions with lambda, pass them to other functions, and capture surrounding state in closures.

Create a file named higher_order.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
;; Anonymous functions are created with LAMBDA; FUNCALL invokes them
(let ((double (lambda (x) (* x 2))))
  (format t "double 21 = ~a~%" (funcall double 21)))

;; MAPCAR applies a function to each element of a list
(format t "squares: ~a~%" (mapcar (lambda (x) (* x x)) '(1 2 3 4 5)))

;; REDUCE folds a list down to a single value (#'* is the * function)
(format t "product: ~a~%" (reduce #'* '(1 2 3 4 5)))

;; REMOVE-IF-NOT keeps only the elements that satisfy a predicate
(format t "evens: ~a~%" (remove-if-not #'evenp '(1 2 3 4 5 6)))

;; A closure: an inner function that captures and mutates outer state
(defun make-counter ()
  (let ((count 0))
    (lambda ()
      (incf count))))

(let ((counter (make-counter)))
  (format t "count: ~a ~a ~a~%"
          (funcall counter)
          (funcall counter)
          (funcall counter)))

The #' reader macro (shorthand for function) retrieves the function object bound to a name, so #'* and #'evenp pass those functions as arguments. make-counter returns a closure: the returned lambda “remembers” its private count variable across calls, incrementing it each time. This is how Common Lisp models encapsulated, stateful behavior without classes.

Running with Docker

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

# Run each example
docker run --rm -v $(pwd):/app -w /app clfoundation/sbcl:latest sbcl --script functions.lisp
docker run --rm -v $(pwd):/app -w /app clfoundation/sbcl:latest sbcl --script parameters.lisp
docker run --rm -v $(pwd):/app -w /app clfoundation/sbcl:latest sbcl --script scope.lisp
docker run --rm -v $(pwd):/app -w /app clfoundation/sbcl:latest sbcl --script recursion.lisp
docker run --rm -v $(pwd):/app -w /app clfoundation/sbcl:latest sbcl --script higher_order.lisp

Expected Output

Running functions.lisp:

square of 5 = 25
add 3 and 4 = 7
7 is odd

Running parameters.lisp:

Hello, Ada!
Hi, Ada!
medium coffee with 1 shot(s)
large coffee with 2 shot(s)
medium coffee with 1 shot(s) (decaf)
sum-all = 15

Running scope.lisp:

with bonus: 110
no bonus:   100
global:     10
5 4 3 2 1 0 

Running recursion.lisp:

10! = 3628800
length = 4
17 / 5 = 3 remainder 2

Running higher_order.lisp:

double 21 = 42
squares: (1 4 9 16 25)
product: 120
evens: (2 4 6)
count: 1 2 3

Key Concepts

  • defun defines named functions — the parameter list follows the name, and the value of the last body expression is returned automatically; there is no return keyword.
  • Rich parameter lists&optional adds defaultable trailing arguments, &key adds named arguments in any order, and &rest collects extra arguments into a list.
  • Lexical scope by defaultlet creates local variables and labels/flet create local functions; global special variables use the *earmuffs* convention and can be dynamically rebound.
  • Recursion replaces loops — walking lists with rest/null and counting down with 1- are idiomatic; recursion is the most natural iteration tool in Lisp.
  • Multiple return valuesvalues lets a function return several results, received with multiple-value-bind, without packing them into a list.
  • Functions are first-class — create them anonymously with lambda, reference named ones with #', and pass them to higher-order functions like mapcar, reduce, and remove-if-not.
  • Closures capture state — a function returned from another function retains access to the variables that were in scope when it was created, enabling private, persistent state.

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