Skip to content

LISP - Remove an Element

Modern Guide to Removing Elements from Sequences in Lisp

Section titled “Modern Guide to Removing Elements from Sequences in Lisp”

Common Lisp provides a robust set of functions for removing elements from sequences (like lists and vectors). A crucial distinction in Lisp is between non-destructive functions, which return a new modified sequence, and destructive functions, which may modify the original sequence. This guide covers the modern best practices for these operations.

Non-destructive functions are safer because they don’t have side effects on the original data. This aligns with functional programming principles and makes code easier to reason about. You should always prefer these unless you have a specific, proven performance bottleneck.

The remove function creates and returns a new sequence with all occurrences of a specific item removed. The original sequence is left untouched.

(remove item sequence &key from-end test test-not start end count key)

Let’s see a practical example. We’ll remove the number 3 from a list.

(let ((original-list '(1 2 3 4 3 5)))
(let ((new-list (remove 3 original-list)))
(format t "Original List: ~a~%" original-list)
(format t "New List: ~a~%" new-list)))
Original List: (1 2 3 4 3 5)
New List: (1 2 4 5)

These functions are more powerful, as they remove elements based on a condition (a predicate function) instead of a specific item.

(remove-if predicate sequence &key ...)
(remove-if-not predicate sequence &key ...)

Example: Remove all odd numbers from a list.

;; #'oddp is a predicate that returns true for odd numbers.
(let ((new-list (remove-if #'oddp '(1 2 3 4 5 6 7))))
(format t "List with odd numbers removed: ~a~%" new-list))
List with odd numbers removed: (2 4 6)

Destructive functions like delete and delete-if may modify the original sequence’s memory structure (its cons cells) for efficiency. This can be dangerous if the sequence is shared elsewhere in your program.

Important Best Practice for Destructive Functions

Section titled “Important Best Practice for Destructive Functions”

Because a destructive function might modify the very first element of a list, the start of the list can change. Therefore, you must always use the return value of a destructive function, typically by assigning it back to the original variable.

;; CORRECT usage of delete
(setq my-list (delete 'a my-list))
;; INCORRECT usage of delete. This can lead to bugs!
(delete 'a my-list) ; The binding of my-list is not updated.

These are the destructive counterparts to remove and remove-if.

Example: Destructively remove even numbers from a list.

(let ((my-numbers '(1 2 3 4 5 6 7)))
(format t "Original variable before delete-if: ~a~%" my-numbers)
;; The list is modified in place, and we re-assign the variable.
(setq my-numbers (delete-if #'evenp my-numbers))
(format t "Variable after delete-if: ~a~%" my-numbers))
Original variable before delete-if: (1 2 3 4 5 6 7)
Variable after delete-if: (1 3 5 7)

The :key argument is incredibly useful for working with complex data structures. It allows you to specify a function to be called on each element before the test is applied.

Example: Remove all products with a price less than 50 from a list of property lists (plists).

(let ((products '((:name "apple" :price 30)
(:name "banana" :price 60)
(:name "orange" :price 45))))
(let ((expensive-products
(remove-if (lambda (price) (< price 50))
products
:key (lambda (product) (getf product :price)))))
(format t "Expensive products: ~a~%" expensive-products)))
Expensive products: ((:NAME "banana" :PRICE 60))