为何Common Lisp内置序列/列表搜索函数不支持多项搜索?
Great question! You’re absolutely right to notice that workhorse functions like position, find, count, member, and assoc all operate on single elements. Let’s break down why this design choice makes sense for Common Lisp, and why it doesn’t have to be a limitation:
1. It’s All About Composable Primitives
Common Lisp’s design leans hard into the idea of small, focused building blocks that you can mix and match to solve bigger problems. Instead of baking every possible use case into the standard library, it gives you flexible primitives that let you build exactly what you need.
For example, if you need to search for multiple elements, you don’t need a brand-new built-in—you can use loop (or mapcar, or iterate if you’re using that library) to wrap the single-element functions:
;; Get positions of multiple items in a sequence (loop for item in '(foo bar baz) collect (position item my-sequence :test #'equal))
This approach is way more flexible than a one-size-fits-all multi-element function. You can tweak the :test parameter per item, handle missing elements differently, or even add side effects without being locked into a fixed implementation.
2. Performance Isn’t As Bad As It Seems (And You Can Optimize When Needed)
You’re right that repeated scans can feel inefficient, but keep in mind:
- Common Lisp implementations (like SBCL, Clozure CL) heavily optimize these core functions—they’re often written in hand-tuned C or assembly under the hood.
- If you do need a single-pass solution, writing a custom function isn’t "repeating built-in work"—it’s just combining logic that’s specific to your use case. For example, here’s a single-pass function to collect positions of multiple items:
(defun multi-position (target-items sequence &key (test #'eql)) (let ((target-table (make-hash-table :test test))) ;; Initialize hash table to track which items we're looking for (dolist (item target-items) (setf (gethash item target-table) nil)) ;; Scan sequence once, recording first positions (loop for elem across sequence for idx from 0 when (gethash elem target-table) do (setf (gethash elem target-table) idx) finally (return (loop for item in target-items collect (cons item (gethash item target-table)))))))
This only touches the sequence once, uses a hash table for fast lookups, and gives you full control over how results are formatted—something a generic built-in couldn’t easily do.
3. Backward Compatibility & Standard Simplicity
ANSI Common Lisp standardized in the late 80s, and many of these single-element functions trace their roots back to earlier Lisp dialects (like Maclisp and Scheme). Adding multi-element versions would have bloated the standard, and there was no consensus on exactly how they should behave (e.g., return all positions? First positions? Handle duplicates? What about different test functions per item?).
The standard prioritizes stability and minimalism—leaving edge cases and specialized use cases to users or third-party libraries.
4. There Are Tools for Multi-Element Work (Just Not Directly)
Don’t overlook that Common Lisp has higher-level functions that handle multiple elements indirectly:
- Use
remove-ifordelete-ifwith a predicate that checks membership in a set:;; Remove all elements in '(a b c) from the sequence (remove-if (lambda (x) (member x '(a b c))) my-sequence) - Set operations like
intersection,union, andset-differencework with entire sequences, effectively handling multi-element comparisons in one pass. - Third-party libraries (like
cl-containersoralexandria) add more specialized multi-element utilities if you need them, without cluttering the core standard.
Wrapping Up
Single-element search functions keep their consistent, optimized behavior because they’re focused on doing one thing well. The language doesn’t force a multi-element abstraction on you because it trusts you to compose the primitives into exactly what you need—whether that’s a quick loop for simple cases, or a custom single-pass function for performance-critical code.
内容的提问来源于stack exchange,提问作者davypough

