如何在ELisp中创建命名参数?

12

我认为由于 Emacs Lisp 和 Common Lisp 在语法上看起来非常相似,所以我可以只需按照我在 RosettaCode 上找到的示例代码进行编写。但事实证明我错了。

相关代码如下:

(defun print-name (&key first (last "?"))
  (princ last)
  (when first
    (princ ", ")
    (princ first))
  (values))

根据RosettaCode的说法,它应该执行以下操作:

> (print-name)
  ?
> (print-name :first "John")
  ?, John
> (print-name :last "Doe")
  Doe
> (print-name :first "John" :last "Doe")
  Doe, John

现在,问题来了:每当我尝试在我的ELisp解释器中运行该函数时,我都会收到以下错误提示:

Now, here's the thing; whenever I try to run that function in my ELisp interpreter, I get the following error:

*** Eval error ***  Wrong number of arguments: (lambda (&key first (last "?")) (princ la\
st) (if first (progn (princ ", ") (princ first))) (values)), 0

我对Lisp不够熟悉,不知道这是什么意思,而且无论怎么搜索也找不到答案。

那么在Emacs Lisp中正确的做法是什么?

3个回答

23

由于Emacs Lisp不直接支持关键字参数,因此您需要模拟这些参数,可以使用cl-defun如其他答案中所示,或者通过将参数解析为plist:

(defun print-name (&rest args)
  (let ((first (plist-get args :first))
        (last (or (plist-get args :last) "?")))
    (princ last)
    (when first
      (princ ", ")
      (princ first))))

&rest args告诉Emacs将所有函数参数放入一个列表中。plist-get从属性列表中提取值,即格式为(key1 value1 key2 value2 …)的列表。实际上,plist是一个扁平的alist。

这使您可以像在问题中一样调用print-name

> (print-name)
  ?
> (print-name :first "John")
  ?, John
> (print-name :last "Doe")
  Doe
> (print-name :first "John" :last "Doe")
  Doe, John

20

Elisp的defun不支持&key(尽管它支持&optional&rest)。有一个宏可以让你使用&key来定义函数。在Emacs 24.3及更高版本中,您可以要求cl-lib并使用cl-defun

(require 'cl-lib)
(cl-defun print-name (&key first (last "?"))
   ...

在早期的Emacs版本中,相同的功能在cl库中以defun*命名。

(require 'cl)
(defun* print-name (&key first (last "?"))
   ...

推荐使用cl-lib库而不是cl,因为它通过在所有符号前加上cl-前缀来保持名称空间的整洁; 但是,如果您需要与早期版本的Emacs兼容,则可以选择cldefun*


示例中的函数还调用了values函数。这个函数特定于Common Lisp,但在cl-lib中可以作为cl-values,在cl中可以作为values使用。

然而,这些替代方法并不完全与它们的Common Lisp对应物相同。Common Lisp具有多返回值的概念。例如,调用(values 1 2 3)将返回三个值-调用上述的(values)将返回零个值。Emacs Lisp函数通过列表模拟多返回值,并重新定义multiple-value-bind以匹配。这意味着调用(cl-values)将只返回nil(空列表)。

在这样的Emacs Lisp函数中,您可以显式地将返回值指定为nil,或者干脆省略它,将最后一个表单的返回值作为函数的返回值,因为调用者不需要使用返回值。


这并不起作用,我收到一个错误,说“符号的函数定义是无效的:values”。 - Electric Coffee
没错,values 函数是另一个 Common Lisp 的特色。在 cl-lib 中它被称为 cl-values - legoscia
这样做只会返回nil而不是打印任何内容... 至少它不会产生错误。 - Electric Coffee

4

不幸的是,elisp本身不支持命名参数。但是,您可以通过将alist传递给函数来模拟该功能,请查看有关alists的指南

在核心上,这意味着您需要将类似于映射的数据结构传递到函数中,因此您需要自己处理包装(将参数组合成alist)和解包装(分解为变量/检查必需/可选值)。

创建输入结构很简单,只需组合变量符号和其值的cons单元格:

(print-name '((first . "John") (last . "Doe")))

阅读更简单:只需使用assoc获取相应的值:

(assoc 'last '((first . "John") (last . "Doe")))
; => (last . "Doe")
(assoc 'middle '((first . "John") (last . "Doe")))
; => nil

因此,print-name 可以长这样:
(defun print-name (alist)
  (let ((first (assoc 'first alist))
        (last (assoc 'last alist)))
    (if last
        (princ (cdr last))
      (error "Missing last name!"))
    (when first
      (princ ", ")
      (princ (cdr first)))))

8
习惯用语上,你最好使用&rest args并将args解析为plist,这样可以减少语法上的混乱:(print-name :first "John" :last "Doe") - user355252

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接