Skip to content

LISP - Custom Predicates

A predicate is a function that answers a true/false question about its input. In Common Lisp, predicates are fundamental. They return T (for true) or NIL (for false). Defining your own clear, robust predicates is key to writing expressive and readable Lisp code.

You define a predicate just like any other function, using defun. By convention, the names of predicate functions end with a -P.

(defun <predicate-name>-p (parameter-list)
"Optional documentation string."
(body-that-evaluates-to-t-or-nil))

A simple predicate that checks if a number is greater than zero.

(defun positive-p (number)
"Returns T if NUMBER is a number greater than zero, otherwise NIL."
(and (numberp number) (> number 0)))
;; --- Tests ---
(print (positive-p 10)) ;=> T
(print (positive-p -5)) ;=> NIL
(print (positive-p 0)) ;=> NIL
(print (positive-p "abc")) ;=> NIL (due to the `numberp` check)

Note: We added (numberp number) to make the function more robust. It now handles non-numeric input gracefully instead of causing an error.

While Lisp has the built-in null function for this, defining it yourself is a good exercise.

(defun empty-list-p (list)
"Returns T if LIST is the empty list '(), otherwise NIL."
(null list))
;; --- Tests ---
(print (empty-list-p '())) ;=> T
(print (empty-list-p nil)) ;=> T (NIL is equivalent to '())
(print (empty-list-p '(a b))) ;=> NIL

Example 3: Checking if a Number is Within a Range

Section titled “Example 3: Checking if a Number is Within a Range”

Common Lisp’s comparison operators like < and <= can take multiple arguments, which leads to very clean and readable code for range checks.

(defun in-range-p (num lower upper)
"Returns T if NUM is between LOWER and UPPER (inclusive)."
(and (numberp num) (numberp lower) (numberp upper)
(<= lower num upper)))
;; --- Tests ---
(print (in-range-p 5 1 10)) ;=> T
(print (in-range-p 10 1 10));=> T
(print (in-range-p 0 1 10)) ;=> NIL

For writing truly robust code, especially for libraries or team projects, it’s better to signal an error on invalid input rather than returning NIL. The check-type macro is perfect for this.

(defun positive-p (number)
"Returns T if NUMBER is a number greater than zero.
Signals a type-error if NUMBER is not a number."
(check-type number number)
(> number 0))
;; (positive-p 10) ;=> T
;; (positive-p "abc") ;=> Signals a correctable TYPE-ERROR

It’s a good practice to write tests for your functions. The assert macro is a simple way to create a basic test suite. It signals an error if its argument expression evaluates to NIL.

(defun test-predicates ()
"Runs a simple test suite for our custom predicates."
;; Test positive-p
(assert (positive-p 5))
(assert (not (positive-p -5)))
(assert (not (positive-p 0)))
;; Test in-range-p
(assert (in-range-p 5 0 10))
(assert (not (in-range-p 11 0 10)))
(format t "All tests passed!"))
;; To run the tests:
;; (test-predicates)
  • Naming Convention: Always use the -p suffix. It makes code self-documenting (e.g., (if (user-active-p user) ...)).
  • Documentation: Write a clear docstring explaining what the predicate checks, its parameters, and what it returns.
  • Be Explicit: Ensure your function always returns either T or NIL. Use (not ...) to invert a condition rather than relying on other “false” values.
  • Leverage Built-ins: Before writing a custom predicate, check if a built-in Lisp function already does what you need (e.g., evenp, oddp, streamp, listp, null).
  • Consider Input Types: Decide whether your predicate should return NIL for invalid types (flexible) or signal an error with check-type (robust).