Common lisp: Can I define a function with an arbitrary number of arguments and optional keywords?

I am new to CL, coming from R and Python.

I want to define a function to which I can pass an arbitrary number of arguments, and also have keywords with default values ​​that I can set if I need different default values.

In R, I can do it like this:

foo <- function(..., a = 1, b = 2){ list(a = a, b = b, ...) } foo(1, 2, 3) foo(1, 2, 3, a = 2) foo(b = 10, 1, 2, 3) 

The PCL says that you can combine the arguments & key and & rest, but that doesn't work as I would expect if I try something like

 (defun foo (&rest rest &key (a 1) (b 2)) (list rest ab)) 

Here I get an error of an unknown type and, if I specify something other than the two args keywords:

 (foo :a 100 12 3) >> unknown &KEY argument: 12 

What I want is similar functionality:

 (defun bar (&optional (a 1) (b 2) &rest rest) (list rest ab)) (bar 5 4 1 2 3 4) >>((1 2 3 4) 5 4) 

But I want to choose whether I pass the argument: a and: b. I am using sbcl.

+7
common-lisp
source share
2 answers

There is no standard way to do this in CL. You can force the function to take everything in the rest parameter and independently analyze the keyword arguments if you do not want to develop the API in different ways.

However, here are a few points for further research. They may be useful in specific use cases, but are quite limited and use implementation-specific behavior.

You can use &allow-other-keys in the lambda list or :allow-other-keys t when calling foo to prevent an unknown key error, but rest will also include the keys and values ​​of your keyword arguments:

 CL-USER> (defun foo (&rest rest &key (a 1) (b 2)) (list rest ab)) FOO CL-USER> (foo) (NIL 1 2) CL-USER> (foo :a 100 12 3 :allow-other-keys t) ((:A 100 12 3 :ALLOW-OTHER-KEYS T) 100 2) CL-USER> (defun foo (&rest rest &key (a 1) (b 2) &allow-other-keys) (list rest ab)) FOO CL-USER> (foo) (NIL 1 2) CL-USER> (foo :a 100 12 3) ((:A 100 12 3) 100 2) 

As correctly indicated in the comment below, this may signal an error. It works for me in CLISP, SBCL, and CCL according to the default optimization settings, but by standard the keyword argument names (i.e. the first of each pair of arguments) must be characters. Regardless of whether it works, it depends on the level of security and depends on the implementation. It should signal an error (about the corresponding implementations) in the secure code (security level 3).

In general, using other keys may be useful for passing keyword arguments, but this is not quite what you wanted. One quick and dirty way is to filter the parameters of keywords in rest and simply discard them and their subsequent elements. Something like that:

 CL-USER> (defun foo (&rest rest &key (a 1) (b 2) &allow-other-keys) (let ((rest (loop for (key value) on rest by #'cddr unless (keywordp key) append (list key value)))) (list rest ab))) FOO CL-USER> (foo) (NIL 1 2) CL-USER> (foo :a 100 12 3) ((12 3) 100 2) 

Which, alas, the standard will only work for an even number of arguments, as his answer indicates. (It may work with an odd number of arguments in some implementations for some security levels, but it did not work in the tested versions.) In addition, it is obvious that it is not reliable in other ways (different positions of keyword arguments, etc.) , and not for use in production, but as a starting point for possible research.

The correct solution should be to write your own argument parser for the arguments and use it with rest or, as stated in the coredump answer, using another API. Another point worth mentioning is that in CL apply large number of arguments is usually not a good idea, as this can lead to inefficient code. To make matters worse, it is also not very reliable, since the number of allowed arguments depends on the implementation. For example, under CCL in my system, call-arguments-limit is 65536. This can be significant - even an order of magnitude more - less with other implementations and systems. Therefore, generally speaking, prefer reduce ing apply large number of arguments.

+5
source share

Basically, this is how variable numbers of arguments are processed in combination with keywords:

  • First, mandatory and optional arguments are bound to variables.
  • Then the rest of the arguments are supplied with the &rest option, if specified; Let's say this parameter is called rest . This is a list of all other arguments.
  • Now, if &key also specified in lambda form, the keywords are extracted from rest , which then must have an even number of arguments (cf. specification ), where the keywords and values ​​alternate, as in (:k1 v1 :k2 v2 ...)
  • If you do not add &allow-other-keys to the lambda shape definition or put :allow-other-keys T when calling it, the number of arguments provided must exactly match the number (and name) of the expected keyword parameters , you can only specify the keyword parameters, declared in lambda form (see @acelant comments for details).

So what can you do?

The easiest approach is to define your functions so that all parameters are bound to keywords with default values. You can also allow the caller to pass additional arguments:

  (defun foo (&rest rest &key (a 1) (b 2) &allow-other-keys) ...) 

This is very similar to your definition, but you cannot just pass values; all arguments must be specified with the key:

  (foo :a 100 :max-depth 12 :max-try 3) ;; for example 

CL is not R or Python: perhaps you do not need to pass as many arguments to your functions, perhaps you can use special variables (dynamic scope), general methods, ... Functions in existing packages usually include mandatory, optional and key parameters. Take the time to read the standard APIs (e.g. cl-ppcre, drakma) and see how you could define your functions in a more idiomatic way.

In conclusion, maybe you really need to define functions with such R-like arguments. If so, you can try defining your own macros for lambda lists in function definition forms so that

  (r-defun FOO ((a 1) (b 2) &rest rest) ...) 

translates to something like:

  (defun FOO (&rest args) (let* ((rest (copy-seq args)) ;; must not modify "args" (a (find-keyword-and-remove-from-rest :a rest :default 1) (b (find-keyword-and-remove-from-rest :b rest :default 2)) ... ;; function body )) 

Do this if you want to enjoy macro exposure, but in fact, I'm sure it is not necessary.


EDIT I initially changed the args argument (previously called rest ) instead of making a copy, but that was a bad example. The specification states :

When a function receives its arguments via & rest, it is acceptable (but not necessary) to implement it in order to bind the rest parameter to an object that shares the structure with the last argument. Since the function cannot determine whether it was called using apply, and also (if so), the last argument to be applied was a constant, the corresponding programs should not rely only on the structure of the list of the remainder list, so that it is freshly baked, or do not change this structure list.

+4
source share

All Articles