常见的lisp:我可以用任意数量的args和可选的关键字args定义一个函数吗?

时间:2014-04-02 08:39:51

标签: common-lisp

我对来自R和Python的CL相当新。

我想定义一个函数,我可以传递任意数量的参数,并且如果我想要默认值中的不同值,也可以设置我可以设置的默认值的关键字。

在R中我可以这样做:

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)

PCL中,它表示你可以将&amp; key和&amp; rest参数组合起来,但如果我尝试类似

的话,它就不会像我期望的那样工作
(defun foo (&rest rest &key (a 1) (b 2))
   (list rest a b))

在这里,如果我指定除两个关键字args之外的任何内容,我会收到一个未知&amp;键错误:

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

我想要的是与此类似的功能:

(defun bar (&optional (a 1) (b 2) &rest rest)
   (list rest a b))

(bar 5 4 1 2 3 4)
>>((1 2 3 4) 5 4)

但我想选择是否提供参数:a和:b。 我正在使用sbcl。

2 个答案:

答案 0 :(得分:7)

在CL中没有标准的方法可以做到这一点。你可以让函数接受rest参数中的所有内容,并自己解析关键字参数,如果你不想以不同的方式设计API。

那就是说,这里有几点需要进一步探索。它们可能在特定用例中很有用,但相当有限并且可以利用与实现相关的行为。

您可以在lambda列表中使用&allow-other-keys,或在调用:allow-other-keys t时使用foo以防止未知密钥错误,但rest还会包含您的密钥和值关键字参数:

CL-USER> (defun foo (&rest rest &key (a 1) (b 2))
           (list rest a b))
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 a b))
FOO
CL-USER> (foo)
(NIL 1 2)
CL-USER> (foo :a 100 12 3)
((:A 100 12 3) 100 2)

正如acelent在下面的评论中正确指出的那样,可能表示错误。它在默认优化设置下适用于CLISP,SBCL和CCL,但是通过标准,关键字参数名称(即每对参数中的第一个)必须是符号。这是否有效取决于安全级别,并且取决于实现。它应该在安全代码(安全级别为3)中发出错误信号(符合实现)。

通常,允许其他键可用于传递关键字参数,但并不完全符合您的要求。一种快速而肮脏的方式可能是在rest中过滤关键字参数,只是删除它们及其后续元素。像这样:

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 a b)))
FOO
CL-USER> (foo)
(NIL 1 2)
CL-USER> (foo :a 100 12 3)
((12 3) 100 2)

对于偶数个参数,标准只会起作用,正如coredump在他的回答中指出的那样。 (对于某些安全级别,可能在一些实现下使用奇数个参数,但它在我测试的实现中没有用。)另外,显然它不是很健壮在其他方面(关键字参数的不同位置等),并不是用于生产用途,而只是作为可能的探索的起点。

正确的解决方案是编写自己的关键字参数解析器,并将其与rest一起使用,或者如coredump的答案中所指出的,使用不同的API。值得一提的另一点是,在CL中,apply大量的参数通常不是一个好主意,因为它可能会导致代码效率低下。更糟糕的是,它也不是很可靠,因为允许的参数数量与实现有关。例如,在我的系统上的CCL下,call-arguments-limit是65536.在其他实现和系统下,它可能显着 - 甚至数量级 - 更小。因此,一般来说,更喜欢reduceapply大量的论点。

答案 1 :(得分:4)

基本上,这里是如何结合关键字处理可变数量的参数:

  • 首先,强制参数和可选参数绑定到变量。
  • 然后,剩余的提供参数绑定到&rest参数,如果给定的话;让我们说参数名为rest。它是所有剩余参数的列表。
  • 现在,如果lambda表单中还提供了&key,则会从rest中提取关键字,然后预计会有偶数个参数(参见关键字和值互换的specification),如(:k1 v1 :k2 v2 ...)中所示。
  • 除非您在lambda表单定义中附加&allow-other-keys或在调用它时提供:allow-other-keys T,否则提供的参数数量必须与预期关键字参数的数量(和名称)完全匹配您只能提供以lambda格式声明的关键字参数(有关详细信息,请参阅@ acelant&#39; s评论)

那么,你能做什么?

最简单的方法是定义您的函数,以便所有参数都绑定到关键字,并使用默认值。您还可以允许调用者传递其他参数:

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

这与您的定义非常相似,但您不能只传递值;所有论据都必须与密钥一起提供:

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

CL不是R也不是Python:也许你不需要为函数传递这么多参数,也许你可以使用特殊变量(动态范围),泛型方法,... 现有包中的函数通常混合使用强制,可选和关键字参数。花点时间阅读标准API(例如cl-ppcre,drakma),看看如何以更惯用的方式定义函数。

总而言之,也许您确实需要使用类似R的参数定义函数。如果是这种情况,您可以尝试在函数定义表单中为lambda列表定义自己的宏,以便

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

被翻译成类似:

    (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
    ))

如果您希望获得宏扩展的乐趣,请执行此操作,但实际上,我非常确定不需要这样做。


编辑我最初修改了args参数(之前名为rest)而不是复制,但这是一个不好的例子。 The specification says

当函数通过&amp; rest接收其参数时,允许(但不是必需)实现将rest参数绑定到与要应用的最后一个参数共享结构的对象。因为函数既不能检测它是否是通过apply调用,也不能检测是否(如果是)最后一个应用的参数是常量,符合的程序既不能依赖于休息列表的列表结构,也不能修改它,也不能修改该列表结构