lispcommon-lispcdr

Get nth cdr of a list without using nthcdr


I'm a Common Lisp beginner struggling with one of my assignment...

One of my CS assignment is to create a function which basically works as the built-in nthcdr function of clisp.

We will call it ncdr:

(ncdr n L) => n being the cdr we want to output, L the list

Examples:

(ncdr 2 '(a b c)) => (c)
(ncdr 0 '(a b c)) => (a b c)
(ncdr -1 '(a b c)) => nil
(ncdr 5 '(a b c)) => nil

So my approach was to set a counter and compare it to n

Here are the conditions I thought of:

if n < 0 -> nil 
if counter < n -> + 1 counter and (ncdr (cdr list)
if counter = n -> output list
if counter > n -> nil 

And here is what I came up with... I don't think it's the most effective way though and, for now, well it doesn't work.

(defun ncdr (n L &aux counter)
   (setq counter 0)
   (cond
      ((atom L) nil)
      ((< n 0) nil)
      ((< counter n) (+ 1 counter (ncdr n (cdr L))))
      ((eq counter n) L)
      ((> counter n) nil) ))

From the hours I spent experimenting with each cond line of my function I know this line doesn't work as intended

((< counter n) (+ 1 counter (ncdr n (cdr L))))

I get an error: nil is not an number

I feel like I'm missing something critical to solve this.


Solution

  • First, the error. In:

    (+ 1 counter (ncdr n (cdr L))))
    

    you are actually summing three values: 1, the value of the variable counter, and the result of the function ncdr applied to two parameters, which, when nil, produces the error (you are summing, for instance, (+ 1 0 nil) and nil is not a number).

    Then, let’s go to the logic of your program.

    Your conditions are basically correct, although they led to a program that could be improved, as we will see later.

    But, you made a logical error in their implementation: counter should change at each recursive call of the function, increasing by 1, while for each call, you reset it to zero at the beginning of the execution of the body of the function. This is because the increment, even if done correctly, is canceled by the assignment (setq counter 0).

    How can we increment such a variable correctly? By adding a new parameter counter to function, and avoiding to reset it to zero each time, but only assigning it to zero at the beginning, in the initial call.

    How could this be done in Common Lisp? Using an optional parameter, not a auxiliary variable, in this way:

    (defun ncdr (n L &optional (counter 0))
      (cond ((atom L) nil)
            ((< n 0) nil)
            ((< counter n) (+ 1 counter (ncdr n (cdr L))))  ; <- Wrong!!! To be corrected
            ((eq counter n) L)
            ((> counter n) nil)))
    

    So, let’s revise the recursive branch of the conditional, to correctly pass the new value in the recursive call (and perform some minor adjustment):

    (defun ncdr (n L &optional (counter 0))
      (cond ((atom L) nil)
            ((< n 0) nil)
            ((< counter n) (ncdr n (cdr L) (+ 1 counter)))  ; or (1+ counter)
            ((= counter n) L)                               ; use = for numbers
            ((> counter n) nil)))
    

    The &optional keyword in the lambda list has the effect that initially you call (ncdr 2 '(a b c)) so counter is assigned to 0, while in the remaining calls the current value is passed along, incremented by 1, so that at the end of recursion it will be equal to n.

    Efficiency

    There is no need to use another variable, counter, since you already have a variable suitable for the recursion: n. In fact, you could reformulate your conditions in this way:

    if the list is empty, return nil
    if n is less then 0, return nil
    if n is 0, return the list
    if n is greater than 0, recur on n-1 and on the cdr of the list
    

    that bring to a program with the following structure:

    (defun ncdr (n L)
      (cond ((null L) nil)
            ((< n 0) nil)
            ((= n 0) L)
            (t (ncdr ...))  ; <- recursive call - to be completed as exercise!