4

I want to write a function that removes trailing nil's from a list. I first tried to write it elegantly with recursion, but ended up like this:

(defun strip-tail (lst)
  (let ((last-item-pos (position-if-not #'null lst :from-end t)))
    (if last-item-pos
      (subseq lst 0 (1+ last-item-pos)))))

; Test cases.
(assert (eq nil (strip-tail nil)))
(assert (eq nil (strip-tail '(nil))))
(assert (equal '(a b) (strip-tail '(a b nil nil))))
(assert (equal '(a nil b) (strip-tail '(a nil b nil))))
(assert (equal '(a b) (strip-tail '(a b))))

It's arguably clear, but I'm not convinced. Is there a more lispy way to do it?

Machavity
  • 30,841
  • 27
  • 92
  • 100
Johan Kotlinski
  • 25,185
  • 9
  • 78
  • 101

6 Answers6

3

Well, a version would be:

  1. reverse the list
  2. remove leading nils
  3. reverse the list

The code:

(defun list-right-trim (list &optional item)
  (setf list (reverse list))
  (loop for e in list while (eq item e) do (pop list))
  (reverse list))

Here is another variant:

  1. iterate over the list and note the position of the first nil which is only followed by nils
  2. return the sub-sequence

the code:

(defun list-right-trim (list &aux (p nil))
  (loop for i from 0 and e in list
    when (and (null p) (null e)) 
    do (setf p i)
    else when (and p e) do (setf p nil))
  (if p (subseq list 0 p) list))
Rainer Joswig
  • 136,269
  • 10
  • 221
  • 346
  • Curious, why the &aux parameter there instead of an explicit let to define p? Otherwise, I'd do basically the same thing (with dolist instead of loop, but that's just style): (defun list-right-trim (list) (let ((last-pos 0) (i 0)) (dolist (item list) (incf i) (when item (setf last-pos i))) (subseq list 0 last-pos))) – Michael H. Jul 10 '09 at 19:37
  • There is no special reason. &aux is just like a LET - with an indentation level less. – Rainer Joswig Jul 10 '09 at 20:09
3
(defun strip-tail (ls)
    (labels ((strip-car (l)
                  (cond ((null l)       nil)
                        ((null (car l)) (strip-car (cdr l)))
                        (t              l))))
        (reverse (strip-car (reverse ls)))))

Sample run (against your test cases):

[1]> (assert (eq nil (strip-tail nil)))
NIL
[2]> (assert (eq nil (strip-tail '(nil)))) ;'
NIL
[3]> (assert (equal '(a b) (strip-tail '(a b nil nil))))
NIL
[4]> (assert (equal '(a nil b) (strip-tail '(a nil b nil))))
NIL
[5]> (assert (equal '(a b) (strip-tail '(a b))))
NIL
[6]> 
dsm
  • 10,263
  • 1
  • 38
  • 72
2

How about this?

(defun strip-tail (lst)
  (if lst
    (let ((lst (cons (car lst) (strip-tail (cdr lst)))))
      (if (not (equal '(nil) lst)) lst))))

...wonder how to make it tail-recursive though, this version would exhaust the stack for large lists.

Johan Kotlinski
  • 25,185
  • 9
  • 78
  • 101
1

Here's what I came up with, assuming you don't mind this being destructive:

(defvar foo (list 'a 'b 'c nil 'd 'e 'nil 'nil 'f nil nil))

(defun get-last-non-nil (list &optional last-seen)
  (if list
      (if (car list)
          (get-last-non-nil (cdr list) list)
          (get-last-non-nil (cdr list) last-seen))
      last-seen))

(defun strip-tail (list)
  (let ((end (get-last-non-nil list)))
    (if (consp end)
        (when (car end) (setf (cdr end) nil) list))))

(strip-tail foo) -> (A B C NIL D E NIL NIL F)
kenm
  • 23,127
  • 2
  • 43
  • 62
  • @kotlinski Ok, now that I bother to check the asserts it seems to pass, though I wouldn't call it 'elegant'. The second assert is a case where it can't be trusted to do its thing in-place, so like nreverse you've got to use the return value. – kenm Jul 09 '09 at 23:28
  • +1 because it's propably the most efficient possible solution. Though, not functional it's still "lispy" in showing the flexibility. – firefrorefiddle Apr 03 '13 at 11:31
0

I tried using recursion but it doesn't work on GNU CL:

(defun strip(lst) 
    (if (null (last lst))
        (strip (butlast lst))            
     lst))

the idea is:

  1. test if the last list element is nil, if so make a recursive call with the last element removed (butlast)
  2. then return the list itself
Svante
  • 50,694
  • 11
  • 78
  • 122
dfa
  • 114,442
  • 31
  • 189
  • 228
-1

Well, this is not really an answer, but I thought I'd put this here as well so it has better visibility.

In your original implementation, do you think non-list items should be handled?

* (strip-tail "abcde")

"abcde"
* (strip-tail 42)

debugger invoked on a TYPE-ERROR in thread #<THREAD "initial thread" {A69E781}>:
  The value 42 is not of type SEQUENCE.
sigjuice
  • 28,661
  • 12
  • 68
  • 93