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:
| |
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:
| |
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:
| |
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:
| |
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:
| |
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
| |
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
defundefines named functions — the parameter list follows the name, and the value of the last body expression is returned automatically; there is noreturnkeyword.- Rich parameter lists —
&optionaladds defaultable trailing arguments,&keyadds named arguments in any order, and&restcollects extra arguments into a list. - Lexical scope by default —
letcreates local variables andlabels/fletcreate local functions; global special variables use the*earmuffs*convention and can be dynamically rebound. - Recursion replaces loops — walking lists with
rest/nulland counting down with1-are idiomatic; recursion is the most natural iteration tool in Lisp. - Multiple return values —
valueslets a function return several results, received withmultiple-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 likemapcar,reduce, andremove-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
Comments
Loading comments...
Leave a Comment