【问题标题】:How do keywords work in Common Lisp?Common Lisp 中的关键字是如何工作的?
【发布时间】:2012-12-11 06:44:34
【问题描述】:

问题不在于使用关键字,而实际上是关键字实现。例如,当我使用关键字参数创建一些函数并进行调用时:

(defun fun (&key key-param) (print key-param)) => FUN
(find-symbol "KEY-PARAM" 'keyword) => NIL, NIL   ;;keyword is not still registered
(fun :key-param 1) => 1
(find-symbol "KEY-PARAM" 'keyword) => :KEY-PARAM, :EXTERNAL

如何使用关键字来传递参数?关键字是自己取值的符号,那么如何用关键字来绑定对应的参数呢?

关于关键字的另一个问题——关键字用于定义包。我们可以定义一个以已有关键字命名的包:

(defpackage :KEY-PARAM) => #<The KEY-PARAMETER package, 0/16 ...
(in-package :KEY-PARAM) => #<The KEY-PARAMETER package, 0/16 ...
(defun fun (&key key-param) (print key-param)) => FUN
(fun :KEY-PARAM 1) => 1

系统如何区分包名和函数参数名:KEY-PARAM的用法? 如果我们定义函数KEY-PARAM 并导出它(实际上不是函数,而是名称),我们也可以让事情变得更复杂:

(in-package :KEY-PARAM)
(defun KEY-PARAM (&key KEY-PARAM) KEY-PARAM) => KEY-PARAM
(defpackage :KEY-PARAM (:export :KEY-PARAM))  
   ;;exporting function KEY-PARAM, :KEY-PARAM keyword is used for it
(in-package :CL-USER) => #<The COMMON-LISP-USER package, ...
(KEY-PARAM:KEY-PARAM :KEY-PARAM 1) => 1
   ;;calling a function KEY-PARAM from :KEY-PARAM package with :KEY-PARAM parameter...

同样的问题,Common Lisp 是如何区分这里关键字:KEY-PARAM 的用法的?

如果有一些关于 Common Lisp 中关键字的手册并解释了它们的机制,如果您在此处发布链接,我将不胜感激,因为我只能找到一些仅关于关键字用法的短文。

【问题讨论】:

    标签: lisp common-lisp


    【解决方案1】:

    有关关键字参数的完整详细信息,请参阅Common Lisp Hyperspec。注意

    (defun fun (&key key-param) ...)
    

    实际上是:

    (defun fun (&key ((:key-param key-param)) ) ...)
    

    关键字参数的完整语法是:

    ((keyword-name var) default-value supplied-p-var)
    

    default-valuesupplied-p-var 是可选的。虽然习惯上使用关键字符号作为keyword-name,但这不是必需的;如果您只指定var 而不是(keyword-name var),则默认keyword-name 是关键字包中与var 同名的符号。

    因此,例如,您可以这样做:

    (defun fun2 (&key ((myoption var))) (print var))
    

    然后将其称为:

    (fun 'myoption 3)
    

    它在内部工作的方式是当函数被调用时,它会遍历参数列表,收集参数对&lt;label, value&gt;。对于每个label,它会在参数列表中查找具有该keyword-name 的参数,并将对应的var 绑定到value

    我们通常使用关键字的原因是因为: 前缀很突出。并且这些变量已经进行了自我评估,因此我们不必也引用它们,即您可以写 :key-param 而不是 ':key-param(仅供参考,后一种表示法在早期的 Lisp 系统中是必需的,但 CL设计师认为它丑陋且多余)。而且我们通常不会使用指定与变量名称不同的关键字的功能,因为这会造成混淆。这样做是为了完全通用。此外,允许常规符号代替关键字对于 CLOS 等工具很有用,其中参数列表被合并并且您希望避免冲突 - 如果您正在扩展通用函数,您可以添加关键字在您自己的包中的参数不会发生碰撞。

    在定义包和导出变量时使用关键字参数再次只是一种约定。 DEFPACKAGEIN-PACKAGEEXPORT等只关心他们的名字,而不关心它在什么包里。你可以写

    (defpackage key-param)
    

    而且它通常也可以正常工作。许多程序员不这样做的原因是因为它在他们自己的包中实习了一个符号,如果这个符号恰好与他们试图从另一个包中导入的符号同名,这有时会导致包冲突。使用关键字将这些参数从应用程序的包中分离出来,避免了这样的潜在问题。

    底线是:当您使用符号并且只关心其名称而不是其身份时,使用关键字通常是最安全的。

    最后,关于以不同方式使用关键字时的区别。关键字只是一个符号。如果在函数或宏只需要普通参数的地方使用它,则该参数的值将是符号。如果您正在调用具有 &amp;key 参数的函数,那么这是唯一一次将它们用作将参数与参数相关联的标签。

    【讨论】:

    • @Barmar "(defun fun (&key key-param) ...) 实际上是:(defun fun (&key ((: key-param key-param)) ) ...)" 除了第一个之后,一个名为“KEY-PARAM”的符号还没有被嵌入到关键字包中(在至少根据OP的成绩单)。如果它是 arglist 被扩展的简写(也许一个实现可以这样做;我不确定它是否被允许),那么该关键字将在宏扩展时被实习。 (这不是特别重要,但它确实指出“泄漏”(或不)一些 defun 的实现。)
    【解决方案2】:

    好的手册是Chapter 21 of PCL

    简要回答您的问题:

    • 关键字在keyword包中导出符号,因此您不仅可以将它们称为:a,还可以称为keyword:a

    • 函数参数列表中的关键字(称为lambda-lists)可能以以下方式实现。在存在&amp;key 修饰符的情况下,lambda 形式会扩展为类似于以下内容:

      (let ((key-param (getf args :key-param)))
        body)
      
    • 当您使用关键字命名包时,它实际上用作string-designator。这是一个 Lisp 概念,它允许传递给某个处理字符串的函数,这些字符串稍后将用作符号(对于不同的名称:包、类、函数等),不仅是字符串,还有关键字和符号.所以,定义/使用包的基本方法实际上是这样的:

      (defpackage "KEY-PARAM" ...)
      

      但你也可以使用:

      (defpackage :key-param ...)
      

      (defpackage #:key-param ...)
      

      (这里#:是一个读取器宏,用于创建不需要的符号;这种方式是首选,因为您不会在此过程中创建不需要的关键字)。

      后两种形式将被转换为大写字符串。所以一个关键字仍然是一个关键字,而一个包将其命名为字符串,从该关键字转换而来。

    总而言之,关键字具有自身的价值,以及任何其他符号。不同之处在于关键字不需要明确限定 keyword 包或其明确用法。作为其他符号,它们可以作为对象的名称。例如,您可以使用关键字命名一个函数,它可以在每个包中“神奇地”访问 :) 有关详细信息,请参阅 @Xach 的 blogpost

    【讨论】:

    • 谢谢! (getf :key-param args) - 真是很好的解释。另外,我尝试了关键字命名的函数,在 Lispworks 中,(defun :test () "hi") 之类的东西会导致错误 Defining function :TEST visible from package KEYWORD.(但有一个调试器选项“无论如何定义它”),而在 SBCL 中一切正常。
    • 欢迎您!我还不得不说,我不小心按错误的顺序写了这个 - 正确的方法是(getf args :key-param) :)
    【解决方案3】:

    “系统”不需要区分关键字的不同用途。它们只是用作名称。例如,想象两个 plist:

    (defparameter *language-scores* '(:basic 0 :common-lisp 5 :python 3))
    (defparameter *price* '(:basic 100 :fancy 500))
    

    产生语言分数的函数:

    (defun language-score (language &optional (language-scores *language-scores*))
      (getf language-scores language))
    

    关键字与language-score 一起使用时,表示不同的编程语言:

    CL-USER> (language-score :common-lisp)
    5
    

    现在,系统如何区分*language-scores* 中的关键字和*price* 中的关键字?绝对没有。关键字只是名称,在不同的数据结构中指定不同的事物。它们并不比自然语言中的同音异义词更显着——它们的使用决定了它们在给定上下文中的含义。

    在上面的例子中,没有什么能阻止我们在错误的上下文中使用函数:

    (language-score :basic *prices*)
    100
    

    语言并没有阻止我们这样做,不那么花哨的编程语言和不那么花哨的产品的关键字是一样的。

    有很多可能性可以防止这种情况发生:首先不允许language-score 的可选参数,将*prices* 放在另一个包中而不将其外部化,关闭词法绑定而不是使用全局特殊*language-scores*而仅公开意味着添加和检索条目。也许仅仅我们对代码库或约定的理解就足以阻止我们这样做。重点是:系统对关键字本身的区分并不是我们想要实现的必要条件。

    您询问的特定关键字用途没有什么不同:实现可能会将关键字参数的绑定存储在 alist、plist、哈希表或其他任何东西中。在包名称的情况下,关键字仅用作包指示符,而包名称作为字符串(大写)可能仅用作代替。实现是否将字符串转换为关键字、关键字转换为字符串或内部完全不同的东西并不重要。重要的只是名称,以及在什么上下文中使用它。

    【讨论】:

    • 谢谢!还有一个问题,是否可以检查实现是否将关键字转换为字符串?是否可以将关键字简化为字符串?我的意思是当 Lisp 阅读器找到一个关键字时,它会将其解析为一种特殊的字符串。
    • 为了检查实现在内部是如何工作的,您必须查看源代码(或详细的实现特定文档)。在包名称的情况下,我假设实现来比较字符串,因为否则(使用默认阅读器设置)案例信息将丢失。虽然两个包“foo”和“Foo”不同,但两个关键字:foo:Foo 却不同。他们都被同一个symbol-name,“FOO”实习。 (但是,还有:|Foo| 可以防止这种情况发生。)
    猜你喜欢
    • 2013-12-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-14
    • 1970-01-01
    • 2023-03-18
    • 2017-06-19
    相关资源
    最近更新 更多