common lisp invoking function without apply leads to odd number of keywords

288 Views Asked by At

i am currently learning common lisp and stumbled upon a question i could not answer my self:

(defun find-all (item seq &rest keyword-args &key (test #'eql)
                     test-not &allow-other-keys)
 (if test-not
   (apply #'remove item seq
          :test-not (complement test-not) keyword-args)
   (apply #'remove item seq
          :test (complement test) keyword-args)))

The function is used to find every element in seq matching item according to a test function. Sadly I dont understand why the function 'apply' was used here. Shouldn't it be possible to just invoke remove without apply? A warnings says: "The function has an odd number of arguments in the keyword portion" if I invoke remove without apply.

I hope you can help me, Thanks in advance!

2

There are 2 best solutions below

0
Rainer Joswig On BEST ANSWER

REMOVE and FIND-ALL

REMOVE takes a bunch of keyword arguments: from-end, test, test-not, start, end, count and key.

Now in the function FIND-ALL you want to modify just one: either test or test-not and then call REMOVE.

How to write the parameter list of FIND-ALL

Now you have basically three options to write the parameter list of FIND-ALL, since it is basically the same as REMOVE with only one argument changed.

  1. list every keyword argument with their defaults and later pass them on with the necessary changed to REMOVE.

  2. list only a rest arguments list, manipulate this argument list and pass the new one via APPLY to REMOVE.

  3. mix of 1. and 2. like in the example above. List only the required arguments and the keyword arguments to modify plus a rest list of the other keyword arguments provided at call time. Call REMOVE via APPLY.

How good are the three possibilities?

Now 1. has the advantage that you get to see a full parameter list for FIND-ALL and that it does not need to cons an argument list. Lisp can check some of that. But you really need to copy all arguments in your parameter list and later in the calls to REMOVE. Possible but not that great.

Then 2. has the disadvantage of not having a visible parameter list for FIND-ALL, but it might be easier to write with a few functions to manipulate argument lists. 3. is relatively easy to write, but also lacks the full parameter list.

Your example

Thus in your example it is version 3 of above:

  • the required arguments are passed in first
  • next are the modified arguments
  • last is the list of all keyword arguments

If you want to pass an existing list of arguments as part of the arguments to the function, then you need APPLY. That's why it is used there. APPLY calls a function with the arguments taken from a list.

CL-USER 1 > (apply #'+ 1 2 '(3 4 5))
15

CL-USER 2 > (let ((numbers '(3 4 5)))
              (apply #'+ 1 2 numbers))
15


CL-USER 3 > (+ 1 2 3 4 5)
15
0
coredump On

Let's take for example the signature of REMOVE:

remove item sequence &key from-end test test-not start end count key 
=> result-sequence

The above means the function accepts 2 mandatory arguments, item and sequence, and additional keywords arguments that should be given with the :key value syntax. If you only give the key or the value, like:

(remove x list :count)

Then it is invalid. A simple test is to look at the number of parameters in keyword position and check that the number of given arguments is even. When it is odd, you know something is wrong.

If you call:

(remove item seq args)

You are in the same case. It does not matter that args is a list in your specific case.

Apply

APPLY is a more generic way of calling a function: its last argument is a list of additional arguments.

Suppose args is bound to (3 4), then:

(apply #'+ 1 args)

is equivalent to:

(+ 1 3 4)

This works with keyword arguments too; if args is the list (:count 1), then:

(apply #'remove item seq args)

is equivalent to:

(remove item seq :count 1)

And here, the number of arguments in keyword position is even.