Average using &rest in lisp

2019-09-14 15:37发布

问题:

So i was asked to do a function i LISP that calculates the average of any given numbers. The way i was asked to do this was by using the &rest parameter. so i came up with this :

(defun average (a &rest b)
   (cond ((null a) nil)
         ((null b) a)
         (t (+ (car b) (average a (cdr b))))))

Now i know this is incorrect because the (cdr b) returns a list with a list inside so when i do (car b) it never returns an atom and so it never adds (+)

And that is my first question:

  • How can i call the CDR of a &rest parameter and get only one list instead of a list inside a list ?

Now there is other thing : When i run this function and give values to the &rest, say (average 1 2 3 4 5) it gives me stackoverflow error. I traced the funcion and i saw that it was stuck in a loop, always calling the function with the (cdr b) witch is null and so it loops there. My question is:

  • If i have a stopping condition: ( (null b) a) , shouldnt the program stop when b is null and add "a" to the + operation ? why does it start an infinite loop ?

EDIT: I know the function only does the + operation, i know i have to divide by the length of the b list + 1, but since i got this error i'd like to solve it first.

回答1:

(defun average (a &rest b)
  ; ...
  )

When you call this with (average 1 2 3 4) then inside the function the symbol a will be bound to 1 and the symbol b to the proper list (2 3 4).

So, inside average, (car b) will give you the first of the rest parameters, and (cdr b) will give you the rest of the rest parameters.

But when you then recursively call (average a (cdr b)), then you call it with only two arguments, no matter how many parameters where given to the function in the first place. In our example, it's the same as (average 1 '(3 4)).

More importantly, the second argument is now a list. Thus, in the second call to average, the symbols will be bound as follows:

  • a = 1
  • b = ((3 4))

b is a list with only a single element: Another list. This is why you'll get an error when passing (car b) as argument to +.

Now there is other thing : When i run this function and give values to the &rest, say (average 1 2 3 4 5) it gives me stackoverflow error. I traced the funcion and i saw that it was stuck in a loop, always calling the function with the (cdr b) witch is null and so it loops there. My question is:

If i have a stopping condition: ( (null b) a) , shouldnt the program stop when b is null and add "a" to the + operation ? why does it start an infinite loop ?

(null b) will only be truthy when b is the empty list. But when you call (average a '()), then b will be bound to (()), that is a list containing the empty list.

Solving the issue that you only pass exactly two arguments on the following calls can be done with apply: It takes the function as well as a list of parameters to call it with: (appply #'average (cons a (cdr b)))

Now tackling your original goal of writing an average function: Computing the average consists of two tasks:

  1. Compute the sum of all elements.
  2. Divide that with the number of all elements.

You could write your own function to recursively add all elements to solve the first part (do it!), but there's already such a function:

(+ 1 2) ; Sum of two elements
(+ 1 2 3) ; Sum of three elements
(apply #'+ '(1 2 3)) ; same as above
(apply #'+ some-list) ; Summing up all elements from some-list

Thus your average is simply

(defun average (&rest parameters)
  (if parameters  ; don't divide by 0 on empty list
      (/ (apply #'+ parameters) (length parameters))
      0))

As a final note: You shouldn't use car and cdr when working with lists. Better use the more descriptive names first and rest.


If performance is critical to you, it's probably best to fold the parameters (using reduce which might be optimized):

(defun average (&rest parameters)
    (if parameters
        (let ((accum
                (reduce #'(lambda (state value)
                            (list (+ (first state) value) ;; using setf is probably even better, performance wise.
                                (1+ (second state))))
                        parameters
                        :initial-value (list 0 0))))
            (/ (first accum) (second accum)))
        0))

(Live demo)

#' is a reader macro, specifically one of the standard dispatching macro characters, and as such an abbreviation for (function ...)



回答2:

Just define average*, which calls the usual average function.

(defun average* (&rest numbers)
  (average numbers))


回答3:

I think that Rainer Joswig's answer is pretty good advice: it's easier to first define a version that takes a simple list argument, and then define the &rest version in terms of it. This is a nice opportunity to mention spreadable arglists, though. They're a nice technique that can make your library code more convenient to use.

In most common form, the Common Lisp function apply takes a function designator and a list of arguments. You can do, for instance,

(apply 'cons '(1 2))
;;=> (1 . 2)

If you check the docs, though, apply actually accepts a spreadable arglist designator as an &rest argument. That's a list whose last element must be a list, and that represents a list of all the elements of the list except the last followed by all the elements in that final list. E.g.,

(apply 'cons 1 '(2))
;;=> (1 . 2)

because the spreadable arglist is (1 (2)), so the actual arguments (1 2). It's easy to write a utility to unspread a spreadable arglist designator:

(defun unspread-arglist (spread-arglist)
  (reduce 'cons spread-arglist :from-end t))

(unspread-arglist '(1 2 3 (4 5 6)))
;;=> (1 2 3 4 5 6)

(unspread-arglist '((1 2 3)))
;;=> (1 2 3)

Now you can write an average* function that takes one of those (which, among other things, gets you the behavior, just like with apply, that you can pass a plain list):

(defun %average (args)
  "Returns the average of a list of numbers."
  (do ((sum 0 (+ sum (pop args)))
       (length 0 (1+ length)))
      ((endp args) (/ sum length))))

(defun average* (&rest spreadable-arglist)
  (%average (unspread-arglist spreadable-arglist)))

(float (average* 1 2 '(5 5)))
;;=> 3.25

(float (average* '(1 2 5)))
;;=> 2.66..

Now you can write average as a function that takes a &rest argument and just passes it to average*:

(defun average (&rest args)
  (average* args))

(float (average 1 2 5 5))
;;=> 3.5

(float (average 1 2 5))
;;=> 2.66..