Skip to content

LISP - Chaining Predicates

In Lisp, a predicate is a function that tests a condition and returns a generalized boolean value: T (true) or NIL (false). Chaining predicates allows you to build complex logic from simple, reusable tests. This is a core technique in functional programming that leads to clear and maintainable code.

The primary tools for combining predicates are the logical operators, which feature short-circuiting evaluation:

  • and: Evaluates forms from left to right. Stops and returns NIL as soon as any form returns NIL. If all forms evaluate to true, it returns the value of the last form.
  • or: Evaluates forms from left to right. Stops and returns the first non-NIL value it encounters. If all forms evaluate to NIL, it returns NIL.
  • not: Takes a single argument. Returns T if the argument is NIL, and NIL if the argument is anything other than NIL.

Instead of writing one-off checks, it’s a best practice to encapsulate complex logic in a dedicated predicate function. Let’s create a function to check if a number is a positive even integer.

(defun positive-even-p (x)
"Predicate that returns T if X is a positive and even integer, otherwise NIL."
(and (integerp x) ; Is it an integer?
(plusp x) ; Is it positive? (more idiomatic than (> x 0))
(evenp x))) ; Is it even?
;; --- Test Cases ---
(format t "Is 10 a positive even number? ~a~%" (if (positive-even-p 10) 'Yes 'No))
(format t "Is 11 a positive even number? ~a~%" (if (positive-even-p 11) 'Yes 'No))
(format t "Is -4 a positive even number? ~a~%" (if (positive-even-p -4) 'Yes 'No))
(format t "Is 'hello a positive even number? ~a~%" (if (positive-even-p 'hello) 'Yes 'No))

Executing the code produces the following clear output:

Is 10 a positive even number? Yes
Is 11 a positive even number? No
Is -4 a positive even number? No
Is 'hello a positive even number? No

Higher-Order Functions for Sequence Predicates

Section titled “Higher-Order Functions for Sequence Predicates”

Lisp provides powerful higher-order functions that apply predicates to sequences (like lists or vectors). This is a cornerstone of the functional style.

  • every: Returns T if the predicate is true for every element in a sequence.
  • some: Returns the first non-NIL result if the predicate is true for at least one element.
  • notany: Returns T if the predicate is false for all elements.
  • notevery: Returns T if the predicate is false for at least one element.

Imagine you have a list of sensor readings and you want to validate that all readings are valid numbers within an acceptable range.

(defun valid-reading-p (reading)
"Predicate to check if a sensor reading is a number between 0 and 100."
(and (numberp reading)
(>= reading 0)
(<= reading 100)))
(let ((dataset-a '(78 92 100 65))
(dataset-b '(88 95 101 70)) ; Contains an out-of-range value
(dataset-c '(66 'error 93))) ; Contains a non-numeric value
(format t "Is dataset A valid? ~a~%" (if (every #'valid-reading-p dataset-a) 'Yes 'No))
(format t "Is dataset B valid? ~a~%" (if (every #'valid-reading-p dataset-b) 'Yes 'No))
(format t "Is dataset C valid? ~a~%" (if (every #'valid-reading-p dataset-c) 'Yes 'No)))

This demonstrates a practical and readable validation check:

Is dataset A valid? Yes
Is dataset B valid? No
Is dataset C valid? No