分类: 软件

  • Y分钟速成Common Lisp

    面向记忆的学习材料

    快速学习并记住Common Lisp编程语言的基础知识

    知识点: Common Lisp的基本语法单元

    题目: Common Lisp的两个基本语法单元是什么?

    选项:
    A. 函数和变量
    B. 类和对象
    C. 原子和S-表达式
    D. 字符串和数字

    正确答案: C

    解析: Common Lisp的两个基本语法单元是原子(atom)和S-表达式。原子是最基本的数据类型,如数字、符号等。S-表达式是由括号包围的表达式,可以包含原子或其他S-表达式。选项A. B、D虽然都是Common Lisp中的概念,但不是最基本的语法单元。

    速记提示: 记住”AS”——Atom和S-expression是Common Lisp的基本语法单元。

    知识点: Common Lisp的注释语法

    题目: 在Common Lisp中,如何编写一个段落注释?

    选项:
    A. 使用单个分号(;)
    B. 使用两个分号(;;)
    C. 使用三个分号(;;;)
    D. 使用四个分号(;;;;)

    正确答案: C

    解析: 在Common Lisp中,注释的规则如下:单个分号(;)用于行末注释、两个分号(;;)用于标准注释、三个分号(;;;)用于段落注释、四个分号(;;;;)用于文件头注释。因此,段落注释应该使用三个分号(;;;)。

    速记提示: “三分段”——三个分号用于段落(paragraph)注释。

    知识点: Common Lisp的基本数据类型

    题目: 在Common Lisp中,表示复数的语法是什么?

    选项:
    A. (complex 1 2)
    B. #C(1 2)
    C. 1+2i
    D. (1, 2)

    正确答案: B

    解析: 在Common Lisp中,复数使用#C(real imag)的语法表示,其中real是实部,imag是虚部。因此,正确的表示方法是#C(1 2)。选项A是函数调用形式,不是字面量;选项C是其他语言(如Python)中的表示方法;选项D是常见的坐标对表示法,但不是Common Lisp的复数表示法。

    速记提示: 记住”#C”——它像一个”复杂(Complex)”的标志,提醒你这是复数表示法。

    知识点: Common Lisp的变量定义

    题目: 在Common Lisp中,如何定义一个全局(动态)变量?

    选项:
    A. (setq var value)
    B. (let ((var value)))
    C. (defvar var value)
    D. (defparameter var value)

    正确答案: D

    解析: 在Common Lisp中,使用defparameter来定义全局(动态)变量。它的语法是(defparameter var value),其中变量名通常用星号(*)包围,这是一个命名约定。选项A是设置已存在变量的值;选项B是创建局部变量;选项C虽然也可以定义全局变量,但通常用于未初始化的变量。

    速记提示: “def-para-meter”——定义参数就是定义全局变量。

    知识点: Common Lisp的结构体定义

    题目: 在Common Lisp中,如何定义一个名为”dog”的结构体,包含name、breed和age三个字段?

    选项:
    A. (struct dog (name breed age))
    B. (defstruct dog name breed age)
    C. (make-struct ‘dog :name :breed :age)
    D. (define-structure dog (name breed age))

    正确答案: B

    解析: 在Common Lisp中,使用defstruct来定义结构体。正确的语法是(defstruct dog name breed age)。这将创建一个名为dog的结构体,具有name、breed和age三个字段。选项A的struct不是Common Lisp的关键字;选项C是创建结构体实例的语法,不是定义结构体;选项D的define-structure不是标准的Common Lisp函数。

    速记提示: “DEFinition of STRUCTure”——defstruct就是用来定义结构体的。

    知识点: Common Lisp的列表操作

    题目: 在Common Lisp中,哪个函数用于将一个元素添加到列表的开头?

    选项:
    A. append
    B. push
    C. cons
    D. add-to-list

    正确答案: B

    解析: 在Common Lisp中,cons函数用于将一个元素添加到列表的开头。它创建一个新的cons单元,其car是新元素,cdr是原列表。例如,(cons 1 ‘(2 3))会返回(1 2 3)。append用于连接列表;push是一个宏,会把一个元素加入到原有的列表楷体并返回;add-to-list不是Common Lisp的标准函数。

    速记提示: “CONStruct”——cons构造新的列表,把新元素放在最前面;Push——就地修改原有列表。

    知识点: Common Lisp的函数定义

    题目: 在Common Lisp中,如何定义一个名为”hello”的函数,接受一个名字参数并返回问候语?

    选项:
    A. (lambda (name) (format nil “Hello, ~a” name))
    B. (defun hello (name) (format nil “Hello, ~a” name))
    C. (define-function hello (name) (format nil “Hello, ~a” name))
    D. (function hello (name) (format nil “Hello, ~a” name))

    正确答案: B

    解析: 在Common Lisp中,使用defun来定义命名函数。正确的语法是(defun hello (name) (format nil “Hello, ~a” name))。这定义了一个名为hello的函数,接受一个name参数,并返回格式化的问候语。选项A是创建匿名函数(lambda函数);选项C和D不是Common Lisp的标准语法。

    速记提示: “DEFine FUNction”——defun用于定义函数。

    知识点: Common Lisp的可选参数

    题目: 在Common Lisp中,如何在函数定义中指定一个可选参数?

    选项:
    A. (defun func (required [optional]))
    B. (defun func (required optional?))
    C. (defun func (required &optional optional))
    D. (defun func (required (optional nil)))

    正确答案: C

    解析: 在Common Lisp中,使用&optional关键字来指定可选参数。正确的语法是(defun func (required &optional optional))。这定义了一个函数,有一个必需参数和一个可选参数。如果调用时不提供可选参数,其默认值为nil。选项A是Python风格;选项B的问号不是Common Lisp的语法;选项D的括号用法不正确。

    速记提示: “&”符号在Lisp中经常用于特殊用途,而”optional”显然表示”可选”。

    知识点: Common Lisp的相等性判断

    题目: 在Common Lisp中,哪个函数用于比较两个数值是否相等?

    选项:
    A. eq
    B. eql
    C. equal
    D. =

    正确答案: D

    解析: 在Common Lisp中,=函数用于比较数值是否相等。它可以比较不同类型的数值,如整数和浮点数。例如,(= 3 3.0)返回t。eq用于比较对象身份;eql类似于eq,但也能正确比较字符和数字;equal用于比较序列和一些其他类型的结构相等性。

    速记提示: 数学中用”=”表示相等,Common Lisp保持了这个直观的符号。

    知识点: Common Lisp的条件语句

    题目: 在Common Lisp中,最基本的条件语句是什么?

    选项:
    A. when
    B. cond
    C. if
    D. case

    正确答案: C

    解析: 在Common Lisp中,if是最基本的条件语句。它的基本形式是(if test then else)。如果test求值为真,则求值then部分,否则求值else部分。when是if的一个变体,没有else分支;cond用于多重条件判断;case用于基于值的多重分支。

    速记提示: 几乎所有编程语言都用”if”作为基本条件语句,Common Lisp也不例外。

    知识点: Common Lisp的符号操作

    题目: 在Common Lisp中,哪个函数用于从字符串创建符号?

    选项:
    A. make-symbol
    B. intern
    C. symbol-name
    D. gensym

    正确答案: B

    解析: 在Common Lisp中,intern函数用于从字符串创建符号。它在当前包中查找或创建一个给定名称的符号。例如,(intern “HELLO”)会返回符号HELLO。make-symbol创建一个新的未引用符号;symbol-name返回符号的名称;gensym生成一个唯一的符号。

    速记提示: “intern”在英语中有”扣留”的意思,这里可以理解为”将字符串扣留在符号表中”。

    知识点: Common Lisp的包系统

    题目: 在Common Lisp中,如何切换到名为”MY-PACKAGE”的包?

    选项:
    A. (use-package ‘my-package)
    B. (in-package :my-package)
    C. (switch-to-package ‘my-package)
    D. (select-package “MY-PACKAGE”)

    正确答案: B

    解析: 在Common Lisp中,使用in-package函数来切换当前包。正确的语法是(in-package :my-package)。这会将当前包设置为MY-PACKAGE。注意符号前面的冒号,这是包名称的常用写法。use-package用于导入其他包的符号;switch-to-package和select-package不是标准Common Lisp函数。

    速记提示: “in”表示”进入”,就像你实际上”进入”了一个新的包。

    知识点: Common Lisp的宏定义

    题目: 在Common Lisp中,如何定义一个简单的宏?

    选项:
    A. (defmacro name (args) body)
    B. (defun macro name (args) body)
    C. (macro-define name (args) body)
    D. (define-macro (name args) body)

    正确答案: A

    解析: 在Common Lisp中,使用defmacro来定义宏。正确的语法是(defmacro name (args) body)。这定义了一个名为name的宏,接受参数args,主体为body。宏在编译时展开,可以用来扩展语言的语法。选项B是函数定义的语法;选项C和D不是Common Lisp的标准语法。

    速记提示: “DEFine MACRO”——defmacro就是用来定义宏的。

    知识点: Common Lisp的类型声明

    题目: 在Common Lisp中,如何声明一个变量的类型为整数?

    选项:
    A. (declare (integer x))
    B. (the integer x)
    C. (type-of x ‘integer)
    D. (setf x (make-instance ‘integer))

    正确答案: A

    解析: 在Common Lisp中,使用declare特殊操作符来进行类型声明。正确的语法是(declare (integer x))。这声明变量x的类型为整数。the用于类型断言,不是声明;type-of返回对象的类型,不是声明;make-instance用于创建类的实例,与类型声明无关。

    速记提示: “声明(declare)”变量的类型,就像在正式场合”声明”某事一样。

    知识点: Common Lisp的错误处理

    题目: 在Common Lisp中,哪个宏用于捕获和处理错误?

    选项:
    A. catch
    B. throw
    C. handler-case
    D. with-errors

    正确答案: C

    解析: 在Common Lisp中,handler-case宏用于捕获和处理错误。它允许你指定不同类型的错误条件和相应的处理代码。基本语法是(handler-case expression (error-type (condition) handler-body))。catch和throw用于非局部退出,不是专门用于错误处理;with-errors不是标准Common Lisp的构造。

    速记提示: “handler”处理”case”情况,正好对应错误处理的场景。

    知识点: Common Lisp的输入输出

    题目: 在Common Lisp中,哪个函数用于格式化输出字符串?

    选项:
    A. printf
    B. format
    C. print-formatted
    D. write-string

    正确答案: B

    解析: 在Common Lisp中,format函数用于格式化输出字符串。它的基本语法是(format destination control-string &rest args)。destination可以是t(标准输出),nil(返回字符串),或者一个流。control-string包含格式化指令,args是要插入的参数。printf是C语言的函数;print-formatted不是标准函数;write-string只是简单地写出字符串,不进行格式化。

    速记提示: “FORMAT”直接表明了这个函数的功能——格式化输出。

    知识点: Common Lisp的循环

    题目: 在Common Lisp中,哪个是最通用和强大的循环构造?

    选项:
    A. do
    B. loop
    C. for
    D. while

    正确答案: B

    解析: 在Common Lisp中,loop宏是最通用和强大的循环构造。它提供了一种类似自然语言的语法来描述复杂的迭代过程。loop可以用于简单的迭代,也可以用于复杂的数据收集和条件迭代。do是一个基本的迭代构造;for通常是loop的一部分;while在Common Lisp中通常通过loop或do来实现。

    速记提示: “LOOP”直观地表示了循环的概念,而且它确实是最灵活的循环构造。

    知识点: Common Lisp的文件操作

    题目: 在Common Lisp中,哪个函数用于打开文件?

    选项:
    A. fopen
    B. open-file
    C. open
    D. with-open-file

    正确答案: C

    解析: 在Common Lisp中,open函数用于打开文件。它的基本语法是(open filename &key direction element-type if-exists if-does-not-exist)。这会返回一个流对象,你可以用它来读取或写入文件。fopen是C语言的函数;open-file不是标准函数;with-open-file是一个宏,用于自动处理文件的打开和关闭,但它内部使用的是open。

    速记提示: “OPEN”直接表明了这个函数的功能——打开文件。

    知识点: Common Lisp的CLOS (Common Lisp Object System)

    题目: 在CLOS中,如何定义一个简单的类?

    选项:
    A. (define-class name (superclasses) slots)
    B. (defclass name (superclasses) (slots))
    C. (make-class ‘name :superclasses superclasses :slots slots)
    D. (class-def name (superclasses) (slots))

    正确答案: B

    解析: 在CLOS中,使用defclass宏来定义类。正确的语法是(defclass name (superclasses) (slots))。name是类名,superclasses是父类列表(可以为空),slots是槽(属性)定义列表。每个槽可以有various选项,如:initform,:accessor等。选项A. C和D都不是CLOS的标准语法。

    速记提示: “DEFine CLASS”——defclass就是用来定义类的。

    知识点: Common Lisp的读取宏

    题目: 在Common Lisp中,哪个函数用于定义新的读取宏?

    选项:
    A. def-read-macro
    B. set-macro-character
    C. define-reader-macro
    D. make-dispatch-macro-character

    正确答案: B

    解析: 在Common Lisp中,set-macro-character函数用于定义新的读取宏。它的基本语法是(set-macro-character char function &optional non-terminating-p)。这会将字符char与函数function关联,使得读取器遇到char时调用function。non-terminating-p参数指定这个宏字符是否是非终止的。选项A和C不是标准函数;选项D用于创建调度宏字符,这是一种更复杂的读取宏。

    速记提示: “SET”一个”MACRO CHARACTER”——设置一个宏字符。

    知识点: Common Lisp的多值返回

    题目: 在Common Lisp中,如何定义一个返回多个值的函数?

    选项:
    A. (defun func () (return-values 1 2 3))
    B. (defun func () (multiple-value-return 1 2 3))
    C. (defun func () (values 1 2 3))
    D. (defun func () (list 1 2 3))

    正确答案: C

    解析: 在Common Lisp中,使用values函数来返回多个值。正确的语法是(defun func () (values 1 2 3))。这定义了一个函数func,它返回三个值:1、2和3。调用者可以使用multiple-value-bind或multiple-value-list来接收这些值。return-values和multiple-value-return不是标准函数。选项D会返回一个列表,这与多值返回不同。

    速记提示: “VALUES”直接表明了这个函数的功能——返回多个值。

    总结

    Common Lisp是一种强大而灵活的编程语言,具有以下特点:

    1. 语法简洁:基于S表达式,使用前缀表示法。
    2. 动态类型:变量可以持有任何类型的值。
    3. 交互式开发:支持REPL(读取-求值-打印循环)。
    4. 强大的宏系统:允许开发者扩展语言。
    5. 面向对象编程:通过CLOS(Common Lisp Object System)支持。
    6. 函数式编程:支持高阶函数和闭包。
    7. 条件系统:提供强大的错误处理机制。
    8. 多值返回:函数可以返回多个值。
    9. 包系统:提供命名空间管理。
    10. 丰富的数据类型:包括列表、向量、哈希表等。

    掌握这些基础知识点将帮助你开始Common Lisp编程之旅。记住,实践是学习编程语言的最佳方式,所以要多写代码,多使用REPL进行实验。


    (defstruct dog name breed age) 在 Common Lisp 中会创建一个名为 dog 的结构体,包含 namebreedage 三个槽(slot,也常被称为字段)。

    然而,为了更全面地理解,需要补充一些细节:

    • defstruct 的功能:defstruct 宏不仅仅是创建结构体,它还会自动生成一系列访问器函数(accessor functions),用于读取和设置结构体的槽。 对于上面的例子,defstruct 会自动创建以下函数:
      • make-dog:用于创建新的 dog 结构体实例。
      • dog-name:用于获取 dog 结构体的 name 槽的值。
      • (setf (dog-name dog-instance) new-name):用于设置 dog 结构体的 name 槽的值。
      • dog-breeddog-age:类似地,用于访问和设置 breedage 槽。
      • dog-p:用于检查一个对象是否为 dog 结构体实例。
    • 可选参数:defstruct 接受许多可选参数,允许你自定义结构体的行为,例如:
      • :conc-name:指定访问器函数的前缀。 例如 (:conc-name "my-dog-") 会生成 my-dog-namemy-dog-breed 等函数。
      • :predicate:指定用于检查对象类型的谓词函数的名称。
      • :type:指定结构体的类型。
      • :print-function:指定用于打印结构体的函数。
    • 例子:
    (defstruct dog name breed age)
    
    (let ((my-dog (make-dog :name "Buddy" :breed "Golden Retriever" :age 3)))
      (format t "My dog's name is ~a.~%" (dog-name my-dog))
      (format t "My dog is a ~a.~%" (dog-breed my-dog))
      (format t "My dog is ~a years old.~%" (dog-age my-dog)))
    
    ;; 输出:
    ;; My dog's name is Buddy.
    ;; My dog is a Golden Retriever.
    ;; My dog is 3 years old.

    defstruct 提供了更丰富的功能,允许更灵活地定义和操作结构体。 理解这些可选参数对于编写高效和可维护的 Common Lisp 代码至关重要。


    在 Common Lisp 中,special 是一种变量类型,用于声明一个变量是特殊变量。特殊变量与普通的局部变量不同,主要体现在以下几个方面:

    特殊变量的特点

    • 动态作用域
    • 特殊变量在它被定义的上下文中具有动态作用域。这意味着,如果在某个函数或代码块中修改了这个变量的值,所有调用这个函数或代码块的地方都能看到这个修改后的值。
    • 使用 declare 声明
    • 使用 (declare (special x)) 可以将变量 x 声明为特殊变量。这样,Lisp 会知道 x 是一个特殊变量,而不是局部变量。
    • 与局部变量比较
    • 普通局部变量在其定义的代码块外是不可见的,而特殊变量在整个动态范围内都是可见的。

    示例

    以下是使用特殊变量的示例:

    (defvar *my-special* 10)  ; 定义一个特殊变量,通常用星号包围命名
    
    (defun modify-special ()
      (declare (special *my-special*))  ; 声明 *my-special* 为特殊变量
      (setf *my-special* (+ *my-special* 5)))  ; 修改特殊变量的值
    
    (modify-special)  ; 调用函数,修改 *my-special*
    (print *my-special*)  ; 输出 15

    总结

    • 特殊变量在整个动态范围内可见,适用于需要在不同函数之间共享状态的情况。
    • 使用特殊变量时,要小心其作用域,以避免不必要的副作用。

  • CloudWeGo三周年回顾:开源生态与社区的蓬勃发展 🚀

    2021年9月,CloudWeGo正式开源,至今已走过了三年的历程。在这段时间里,CloudWeGo经历了来自各个方向的挑战与考验,始终秉持着开源的初心,为社区贡献了高质量的微服务项目。正如字节基础架构负责人赵鹏伟在三周年活动中所言,我们希望将字节多年的微服务实践经验反馈给社区,让更多企业用户和开发者同样享受到高性能微服务框架带来的益处。

    CloudWeGo的项目生态 🛠️

    CloudWeGo的生态涵盖了多种技术与框架,主要支持两种编程语言——Go和Rust。Go语言自2014年引入字节跳动以来,因其优秀的性能逐渐成为内部主要的业务开发语言,超过50%的服务使用Go开发。而Rust则在三年前开始建设,已经在多个业务线取得了显著的成果。

    在过去三年中,CloudWeGo逐步开源了一系列高性能的项目,其中包括:

    • Kitex:高性能的Golang RPC框架
    • Hertz:高性能的Golang HTTP框架
    • SonicNetpoll:高性能的编解码库和网络库
    • MonoioVolo:Rust语言的高性能框架

    此外,CloudWeGo还在不断扩展其生态系统,支持与CNCF标准的兼容性,确保用户可以灵活集成各类主流开源生态。

    社区与开发者的共建 🌐

    在CloudWeGo的生态发展中,社区开发者的贡献不可或缺。项目中集成了多个复杂的业务案例,例如:

    • EasyNote:集成了RPC与HTTP框架的简单笔记服务,展示了如何使用Hertz和Kitex。
    • Open Payment Platform:展示了如何基于CloudWeGo构建API Gateway。
    • Bookinfo:重写了Istio经典的微服务demo,利用CloudWeGo技术栈展示其强大的功能。
    • Book Shop:一个电商最小化demo,帮助开发者理解电商和CloudWeGo技术栈。

    这些项目不仅展示了CloudWeGo的灵活性与强大功能,也为开发者提供了宝贵的实践经验。

    企业用户案例 📊

    CloudWeGo致力于支持真实的企业用户落地,已经有超过60家企业成功实施了Kitex和Hertz。企业用户反馈显示,使用CloudWeGo后在性能、成本和稳定性方面均获得了显著的改善。例如:

    • 贪玩游戏:通过PHP重构为Golang微服务,显著提升了性能与稳定性。
    • 方正证券:在微服务架构下,利用CloudWeGo的能力实现了服务的稳定运行与治理。
    • 数美科技:通过Kitex框架重构RPC服务,提升了系统的稳定性与扩展能力。

    未来展望 ✨

    CloudWeGo的未来将继续关注社区的反馈与需求,推动技术的进一步发展与落地。我们希望通过不断的努力,为更多开发者和企业提供更强大、更高效的微服务框架。

    在此,我们也鼓励更多的开发者加入到CloudWeGo的开源社区中来,共同推动这一生态的繁荣发展。感谢每一位参与者,让我们携手并进,迎接下一个三年!

  • Linux 内核中的 Rust:风波乍起,未来可期

    🎉 引言:33 年的积淀,新的语言之争
    Linux 内核自 1991 年问世以来,已经走过了 33 年的风雨历程。作为全球最流行的开源操作系统之一,Linux 在技术社区中享有至高的地位。然而,伴随着 2022 年 Linux 内核宣布引入 Rust 语言,这一平稳的发展轨迹忽然掀起了一场新的风波。Rust 语言的引入是否会彻底改变 Linux 内核的未来?这场 “Rust 之争” 究竟会如何演变?我们不妨通过 2024 年维护者峰会的讨论,一探究竟。


    🚀 Rust 的引入:技术发展还是社区内部分裂?
    自从 Rust 被引入 Linux 内核以来,讨论声不断。尤其引发关注的是 Rust for Linux 项目的核心维护者 Wedson Almeida Filho 的退出。他直言不讳地表达了对社区中技术之外争论的厌倦,尤其是围绕 Rust 和 C 语言的争论。Wedson 的离开仿佛为这场语言之争添了一把火,甚至有人认为 Rust 的引入分裂了 Linux 社区。对此,连 Linus Torvalds 也感到疑惑:“为什么现在还有这么多人对 Rust 产生如此大的争议?”

    面对这种局势,Linux 内核维护者们在 2024 年的维护者峰会上展开了深入讨论。Miguel Ojeda 和 Linus Torvalds 纷纷现身回应,试图厘清 Rust 在内核中的现状,并探讨是否是时候将其从“实验性项目”转变为更正式的内核组成部分。


    💡 灵活性需求与期望分歧:核心 API 的调整
    Miguel Ojeda 在峰会上首先提到了内核子系统维护者需要展现的灵活性。两年前,在首次引入 Rust 支持时,他曾指出,由于 Rust 的特性,某些核心 API 需要修改以融入 Rust 代码。如今,这种灵活性变得更加重要。

    他还提到,社区对于 Rust 在内核中的期望存在明显分歧。有些开发者和公司希望看到 Rust 在内核中取得成功,但对其未来发展仍存疑虑。Jason Gunthorpe 表示,尽管 Rust 的引入是为了展示其在内核中的可行性,但他仍在等待明确的信号,证明其成功。


    ⚙️ 工具与支持:编译器、驱动与子系统的挑战
    Rust 的引入不仅仅是语言的替换,还涉及到工具链和子系统的支持。Arnd Bergmann 提出了一个关键问题:“何时才能使用发行版自带的 Rust 编译器来构建内核代码?” 对此,Ojeda 回答道:“内核代码现在支持多个编译器版本,许多社区导向的发行版已经提供了合适的编译器。”

    但是,问题远不止于此。Greg Kroah-Hartman 提到,Rust 开发者主要集中在设备驱动程序的开发上,由于驱动程序需要与许多其他子系统交互,大量支持代码需要合并,这也使得进展看似缓慢。


    🧑‍💻 开发者的困惑与支持:谁来修复破坏的代码?
    随着 Rust 在内核中逐渐扩展,开发者们也面临着新的挑战。Will Deacon 问道:“Rust 社区是否在为内核开发者提供足够的支持?” Ojeda 回应道,他正在组建一个专家团队,其中一些成员是 Rust 的核心开发者,虽然他们缺乏深厚的内核经验,但可以帮助审核补丁和代码。

    Linus Torvalds 也指出,目前内核中的一些特性与 Rust 不兼容,这阻碍了 Rust 的支持进程。例如,modversions 模块版本控制就是当前的一个挑战。尽管如此,阻碍特性的列表正在缩短,未来阻碍 Rust 的障碍会越来越少。


    📉 管理期望:Rust 社区的耐心与现实
    Dan Williams 提到,新的功能合并到内核中需要时间。他曾用两年时间才让一个新的 mmap() 标志被合并。他认为,Rust 社区在这方面需要管理期望,合并 Rust 代码是一个缓慢的过程。

    Ted Ts’o 也表达了类似的观点。他认为,Rust 开发者一直在避免吓到内核维护者,很多人认为只需要学一点点 Rust 就能上手,但实际情况远比这复杂。文件系统抽象涉及到复杂的锁定规则,开发者需要详细的文档和教程来帮助他们用 Rust 编写文件系统代码。


    🔧 未来展望:Rust 在 Linux 内核中的生产级应用
    Linus Torvalds 对 Rust 的未来发展持乐观态度,但也坦言目前内核中的任何功能都不依赖 Rust,短期内也不会依赖。他强调,现在的重点是向前推进,开发者们应该全速前进,不必过于担忧细节问题。Torvalds 表示:“只要能让功能正常运行就足够了。一旦用户开始依赖 Rust 代码,才需要更细致地处理这些问题。”

    Thomas Gleixner 认为,Rust 开发者在记录文档方面非常认真,他不担心重构代码的问题。即便遇到不理解的地方,他也可以像处理 C 代码一样,直接发邮件询问开发者。


    总结:耐心等待,Rust 的未来可期
    对于 Linux 内核中的 Rust 项目,Linus Torvalds 和维护者们一致认为,Rust 进入生产环境是不可避免的,但这个过程需要数年时间。尽管目前还存在许多技术挑战和社区分歧,但随着 Rust 在内核中的逐步推进,未来的 Linux 内核必然会变得更加安全和高效。

    如 Linus 所言:“Rust 已经部分集成到内核两年了,这不算什么。用 Clang 构建内核的项目花了十年,而那还是用的同一种语言。” 未来几年,Rust 的发展值得期待,Linux 内核的前景也将更加广阔。


    📚 参考文献

    1. Jonathan Corbet, “Committing to Rust in the kernel”, LWN.net, September 24, 2024.
    2. Wedson Almeida Filho, “Rust for Linux: Why I Left”, Personal Blog, 2023.
    3. Greg Kroah-Hartman, “Linux Kernel Development”, O’Reilly Media, 2022.
    4. Linus Torvalds, “The Future of Linux Kernel”, Open Source Summit Europe, 2024.
  • 🌿 探索Lisp宏:从基础到进阶

    “扩展你的心灵,去理解我们必须和地球和平共处;伸出你的手,帮助全人类的和谐。”
    —— Lonnie Liston Smith And The Cosmic Echoes

    🧠 前言:宏是什么?

    宏(Macro)是Lisp中最富有魔力的特性之一。它不仅仅是一个函数,宏的输出是代码,随后这些代码会被执行。换句话说,宏允许你写出可以生成和操作代码的代码。显然,这是一项既让人感到困惑,也令人着迷的工具。

    🌟 一个简单的例子:宏与函数的对比

    来看一个简单的宏定义:

    (defmacro my-mac ()
      (print "hello world"))
    (my-mac)

    输出:

    "hello world"
    => "hello world"

    再来看看类似的函数:

    (defun my-func ()
      (print "hello world"))
    (my-func)

    输出的结果是一样的:

    "hello world"
    => "hello world"

    但是不同之处在于,宏生成的是代码,而函数则直接执行代码。换句话说,宏的输出结果会再被当作代码执行,而函数的结果则是直接的输出。

    🤹‍♂️ 宏生成代码

    要更好地理解这一点,我们可以修改宏和函数,让它们输出不同的内容:

    (defmacro my-mac ()
      '(print "hello world"))
    (my-mac)

    输出:

    "hello world"
    => "hello world"

    而如果我们用函数来做同样的事情:

    (defun my-func ()
      '(print "hello world"))
    (my-func)

    输出:

    => (PRINT "hello world")

    在宏的例子中,宏首先生成一个列表 (print "hello world"),然后这个列表被当作代码执行;而函数的结果只是一个列表,它并不会再被执行。简而言之,宏是在编译时生成代码的,而函数是在运行时执行代码。

    🛠️ 编写代码的代码

    Lisp的宏不仅可以生成代码,还可以操作代码,这是因为Lisp的数据与代码共享相同的结构(即Lisp的同构性,homoiconicity)。这意味着我们可以像操作数据那样操作代码。来看一个例子:

    (defmacro example ()
      (let ((stack nil))
        (push 'print stack)
        (push "hello world" stack)
        (reverse stack)))
    (example)

    输出:

    "hello world"
    => "hello world"

    在这个例子中,我们用Lisp的列表操作函数生成了一段代码。宏的输出结果是一个 (print "hello world") 列表,然后这个列表会被执行。

    🎩 引入反引号与逗号

    在Lisp中,反引号(backquote,`)和逗号(comma,,)是我们生成代码的强大工具。反引号允许我们像单引号那样生成列表,而逗号可以动态地插入计算结果。来看一个简单的例子:

    (defun hello (name)
      `(print ,name))
    (hello "David")

    输出:

    => (PRINT "David")

    这个例子生成了一个含有两个元素的列表:PRINT 和字符串 "David"

    我们还可以使用逗号和 format 函数来生成更复杂的代码:

    (defun hellof (name)
      `(print ,(format nil "Hello ~A" name)))
    (hellof "David")

    输出:

    => (PRINT "Hello David")

    🤖 作为宏的例子

    让我们把这个函数转换为宏:

    (defmacro hellom (name)
      `(print ,(format nil "Hello ~A" name)))
    (hellom "David")

    输出:

    "Hello David"
    => "Hello David"

    在这个宏中,我们利用了反引号和逗号来动态插入名字,并生成 print 语句。

    🙃 宏与函数的另一个区别:参数的评估

    函数和宏的另一个重要区别在于参数的评估方式。在函数中,参数会被评估:

    (hellof David)

    这会引发错误:

    [Condition of type UNBOUND-VARIABLE]

    因为 David 这个变量并没有定义。

    而在宏中,参数不会被评估:

    (hellom David)

    输出:

    "Hello DAVID"
    => "Hello DAVID"

    在宏的例子中,David 被当作符号传递给宏,而不是变量值。

    🍕 逗号拼接:在列表中插入列表

    另一个有用的工具是逗号拼接(comma-splice,@),它允许我们将一个列表插入到另一个列表中。来看一个例子:

    (defmacro splice-in (to-splice)
      `(print '(1 2 3 ,@to-splice 4 5)))
    (splice-in (9 9 9))

    输出:

    => (1 2 3 9 9 9 4 5)

    在这里,我们使用了 ,@ 来将列表 (9 9 9) 插入到另一个列表中。

    🤔 什么时候使用宏?

    宏的用途非常广泛,以下是一些常见的使用场景:

    • 新操作符:宏允许创建不评估参数的新操作符。
    • 语法糖:宏可以用来简化代码的语法,使代码更易读。
    • 修改参数:宏可以在参数评估之前对其进行操作。
    • 短路参数:宏可以控制参数的执行顺序或跳过某些参数的评估。
    • 编译时计算:宏可以在编译时执行一些操作,从而提高运行时性能。
    • 条件编译:根据编译时的条件来生成不同的代码。
    • 编译时优化:在编译时预先计算某些值,以减少运行时的开销。
    • 代码生成:宏可以自动生成重复的代码,从而减少代码重复。
    • 减少代码重复:通过宏来生成类似的代码,避免手动编写相似代码块。
    • 连接模型与视图:宏可以在不同的模块之间生成代码进行连接。

    🚀 宏的未来之路

    这篇文章只是宏的入门。Lisp的宏系统是极具扩展性的工具,可以让你创建自己的DSL(领域特定语言),甚至改变语言本身的行为。学习宏不仅能让你更好地理解Lisp,还能让你编写出更加简洁、优雅且高效的代码。


    面向记忆的学习材料

    快速学习并记住Common Lisp宏的基本概念、用法和应用场景

    知识点: 宏的定义
    题目: 在Common Lisp中,宏(macro)是什么?
    选项:
    A. 一种数据结构
    B. 一种输出源代码的函数
    C. 一种循环语句
    D. 一种变量声明方式

    正确答案: B
    解析: 根据教程内容,Lisp宏是一种输出源代码的函数,这个输出的源代码通常会被执行。宏允许我们在编译时生成和操作代码,这是宏的核心特性。
    速记提示: 记住”宏=代码生成器”。宏就像一个魔术师,能变出新的代码。

    知识点: 宏与函数的区别
    题目: 以下哪项不是宏与普通函数的区别?
    选项:
    A. 宏的参数不会被求值
    B. 宏的输出会被作为代码执行
    C. 宏只能在编译时使用
    D. 宏可以生成代码

    正确答案: C
    解析: 宏和函数的主要区别是:(1)宏的参数不会被求值;(2)宏的输出会被作为代码执行;(3)宏可以生成代码。宏不仅限于编译时使用,也可以在运行时展开和执行。
    速记提示: 记住”MAC”:M(Macro不求值),A(Arguments as is),C(Code generation)。

    知识点: 宏展开
    题目: 在Common Lisp中,宏展开(macro expansion)指的是什么过程?
    选项:
    A. 宏定义被编译的过程
    B. 宏被调用时参数求值的过程
    C. 宏输出的代码被执行的过程
    D. 宏生成源代码的过程

    正确答案: D
    解析: 宏展开是指宏生成源代码的过程。当宏被调用时,它会先生成(展开成)一段源代码,然后这段代码会被执行。这就是所谓的宏展开。
    速记提示: 想象宏是一个”代码种子”,展开就是这个种子”生长”成完整代码的过程。

    知识点: 反引用语法
    题目: 在Common Lisp宏定义中,反引用(backquote)符号”"的主要作用是什么? **选项:** A. 创建字符串 B) 创建列表并允许在其中求值 C) 定义函数 D) 声明变量 **

    正确答案:** B **
    解析:** 反引用符号"“主要用于创建列表,并且允许在这个列表中使用逗号”,”来求值某些元素。这提供了一种方便的方式来构建包含动态内容的列表或代码结构。
    速记提示: 反引用就像一个”模板”,里面可以插入动态内容(用逗号)。

    知识点: 逗号在宏中的作用
    题目: 在使用反引用语法定义宏时,逗号”,”的作用是什么?
    选项:
    A. 分隔列表元素
    B. 结束宏定义
    C. 在反引用结构中求值下一个表达式
    D. 声明局部变量

    正确答案: C
    解析: 在反引用结构中,逗号”,”用于求值(evaluate)其后的表达式。这允许我们在主要被引用的结构中插入动态计算的值。
    速记提示: 逗号就像反引用模板中的”计算窗口”,允许执行计算并插入结果。

    知识点: 逗号-@语法
    题目: 在Common Lisp宏中,”,@”(逗号-@)语法的作用是什么?
    选项:
    A. 创建新的列表
    B. 将一个列表拼接到另一个列表中
    C. 声明全局变量
    D. 定义新的函数

    正确答案: B
    解析: “,@”语法称为逗号拼接(comma-splice),它用于将一个列表的内容拼接到另一个列表中。这在构建复杂的宏展开时非常有用。
    速记提示: 想象”,@”是一个”列表融合器”,它能将一个列表无缝地融入另一个列表。

    知识点: 宏参数求值
    题目: 关于宏的参数,以下哪个说法是正确的?
    选项:
    A. 宏的参数在传入前会被求值
    B. 宏的参数不会被自动求值
    C. 宏的参数只有在使用时才会被求值
    D. 宏不能接受参数

    正确答案: B
    解析: 与函数不同,宏的参数在传入时不会被自动求值。这使得宏可以操作原始的代码结构,而不是求值后的结果。
    速记提示: 宏参数就像是”原料”,而不是”成品”,它们保持原样直到宏决定如何使用它们。

    知识点: 宏的应用场景
    题目: 以下哪种情况最适合使用宏而不是函数?
    选项:
    A. 需要进行复杂的数学计算
    B. 需要创建自定义的控制结构
    C. 需要读取文件内容
    D. 需要连接数据库

    正确答案: B
    解析: 宏最适合用于创建自定义的控制结构,因为宏可以控制其参数的求值时机和方式。这对于实现如条件语句、循环等控制结构非常有用。
    速记提示: 宏就像语言的”定制裁缝”,能为语言量身定制新的语法结构。

    知识点: 宏的编译时计算
    题目: 使用宏进行编译时计算的主要优势是什么?
    选项:
    A. 提高运行时性能
    B. 减少内存使用
    C. 简化错误处理
    D. 改善代码可读性

    正确答案: A
    解析: 宏允许在编译时进行计算,这可以将一些计算从运行时转移到编译时,从而提高程序的运行时性能。这对于需要频繁执行的代码段特别有用。
    速记提示: 把宏想象成”预制菜”,在编译这个”烹饪”阶段就完成了一部分工作,所以”用餐”(运行)时会更快。

    知识点: 宏的代码生成能力
    题目: 使用宏进行代码生成的主要目的是什么?
    选项:
    A. 增加代码的复杂性
    B. 减少代码重复
    C. 降低程序的可维护性
    D. 增加运行时的内存使用

    正确答案: B
    解析: 宏的代码生成能力主要用于减少代码重复。通过编写能生成重复代码模式的宏,我们可以显著减少手动编写的代码量,提高代码的可维护性和一致性。
    速记提示: 把宏想象成”代码复印机”,能快速生成多份相似但略有不同的代码。

    知识点: 宏的语法糖作用
    题目: 在Common Lisp中,使用宏创建”语法糖”的主要目的是什么?
    选项:
    A. 提高代码的执行效率
    B. 增加语言的表达能力
    C. 减少内存使用
    D. 简化错误处理过程

    正确答案: B
    解析: 使用宏创建”语法糖”的主要目的是增加语言的表达能力。语法糖允许程序员以更简洁、更直观的方式编写代码,而不改变语言的基本功能。
    速记提示: 把语法糖想象成编程语言的”方言”,让你用更地道、更简洁的方式表达相同的意思。

    知识点: 宏与homoiconicity
    题目: Common Lisp的哪个特性使得宏变得特别强大?
    选项:
    A. 动态类型
    B. 垃圾回收
    C. Homoiconicity(同像性)
    D. 多重分派

    正确答案: C
    解析: Homoiconicity(同像性)是使Common Lisp宏特别强大的特性。它指的是代码和数据具有相同的表示形式(都是S表达式),这使得用代码操作代码变得非常自然和强大。
    速记提示: 想象代码和数据说着”同一种语言”,所以代码可以轻松地”交谈”和操作其他代码。

    知识点: 宏的卫生问题
    题目: 在编写Common Lisp宏时,需要特别注意避免哪种问题?
    选项:
    A. 栈溢出
    B. 变量捕获
    C. 死锁
    D. 内存泄漏

    正确答案: B
    解析: 在编写宏时,需要特别注意避免变量捕获问题。这是指宏引入的变量可能意外地与使用宏的上下文中的变量名冲突,从而导致意外的行为。
    速记提示: 把宏想象成一个”访客”,要小心不要让它的”行李”(变量)与”主人家”(使用环境)的东西搞混。

    知识点: gensym函数
    题目: 在Common Lisp中,gensym函数通常用于解决宏编写中的什么问题?
    选项:
    A. 内存管理
    B. 变量捕获
    C. 类型检查
    D. 异常处理

    正确答案: B
    解析: gensym函数用于生成唯一的符号,通常用于解决宏编写中的变量捕获问题。通过使用gensym生成的唯一符号,可以确保宏引入的变量名不会与外部代码冲突。
    速记提示: 把gensym想象成给变量起”绰号”的工具,确保宏里的变量名字独一无二,不会撞车。

    知识点: 宏的调试
    题目: 在Common Lisp中,哪个函数可以用来查看宏展开的结果?
    选项:
    A. expand-macro
    B. macroexpand
    C. debug-macro
    D. show-expansion

    正确答案: B
    解析: macroexpand函数用于查看宏展开的结果。这个函数接受一个宏调用形式,返回该宏完全展开后的代码。这对于调试和理解复杂的宏非常有用。
    速记提示: macro+expand,直观地表示”展开宏”的动作。

    知识点: 宏的递归展开
    题目: 如果一个宏在其展开中调用了自身,这种情况被称为什么?
    选项:
    A. 宏循环
    B. 递归宏
    C. 自展开宏
    D. 嵌套宏

    正确答案: B
    解析: 当一个宏在其展开中调用了自身,这种情况被称为递归宏。递归宏可以用来处理具有递归结构的问题,但需要小心处理以避免无限递归。
    速记提示: 想象宏是一个套娃,打开一个里面还有一个一模一样的,这就是递归宏的形象比喻。

    知识点: 读取宏(reader macros)
    题目: Common Lisp中的读取宏(reader macros)主要用于什么目的?
    选项:
    A. 改变代码的执行顺序
    B. 扩展语言的语法
    C. 优化代码执行速度
    D. 管理内存分配

    正确答案: B
    解析: 读取宏(reader macros)主要用于扩展Common Lisp的语法。它们允许程序员自定义Lisp读取器的行为,从而引入新的语法结构或简写形式。
    速记提示: 把读取宏想象成语言的”方言创造器”,允许你为语言添加新的”口音”或表达方式。

    知识点: 宏的使用原则
    题目: 在Common Lisp编程中,关于宏的使用,以下哪个原则是正确的?
    选项:
    A. 总是优先使用宏而不是函数
    B. 只在绝对必要时才使用宏
    C. 宏应该完全取代函数
    D. 宏只能用于系统级编程

    正确答案: B
    解析: 正确的宏使用原则是只在绝对必要时才使用宏。虽然宏非常强大,但它们也增加了代码的复杂性,可能导致难以调试的问题。因此,如果一个任务可以用函数完成,通常应该优先使用函数。
    速记提示: 把宏想象成”魔法”,强大但难以控制,所以要谨慎使用,”不到万不得已,不用魔法”。

    知识点: 宏的性能影响
    题目: 关于宏对程序性能的影响,以下哪个说法是正确的?
    选项:
    A. 宏总是会提高程序的运行速度
    B. 宏会降低程序的编译速度但可能提高运行速度
    C. 宏对性能没有任何影响
    D. 宏总是会降低程序的运行速度

    正确答案: B
    解析: 宏可能会降低程序的编译速度,因为需要在编译时进行额外的处理。但是,通过将一些计算从运行时转移到编译时,宏可能会提高程序的运行速度。这种权衡需要根据具体情况来评估。
    速记提示: 想象宏是”预制菜”,可能需要更长的”烹饪”(编译)时间,但可能让”用餐”(运行)更快。

    知识点: 宏与CLOS(Common Lisp Object System)
    题目: 在Common Lisp中,宏如何与CLOS(Common Lisp Object System)交互?
    选项:
    A. 宏不能用于CLOS
    B. 宏可以用来生成CLOS代码
    C. CLOS完全取代了宏的功能
    D. 宏只能用于修改CLOS类定义

    正确答案: B
    解析: 宏可以与CLOS很好地配合,特别是可以用来生成CLOS代码。例如,宏可以用来自动生成方法定义、简化类定义语法,或者实现面向方面编程(AOP)等高级功能。
    速记提示: 把宏想象成CLOS的”助手”,能帮助简化和自动化面向对象编程中的某些任务。

    总结

    Common Lisp的宏是一种强大的元编程工具,允许程序员扩展语言的语法和功能。宏的核心特性包括:

    1. 宏是输出源代码的函数,其输出会被作为代码执行。
    2. 宏的参数不会被自动求值,使得宏能操作原始代码结构。
    3. 反引用语法(`)和逗号(,)提供了构建包含动态内容的代码的便捷方式。
    4. 宏可用于创建新的控制结构、进行编译时计算、生成代码以减少重复,以及创建语法糖。
    5. Homoiconicity(同像性)使得Lisp的宏特别强大。
    6. 使用宏时需要注意变量捕获等问题,可以使用gensym等技术来避免。
    7. 宏应该谨慎使用,只在必要时才采用,因为它们可能增加代码的复杂性。
    8. 宏可能影响编译速度,但可能提高运行速度。
    9. 宏可以与CLOS很好地配合,用于生成和简化面向对象编程代码。

    掌握宏需要深入理解Lisp的语言特性和编程哲学,但它能极大地增强程序员的表达能力和问题解决能力。

    参考文献

    1. Common Lisp – The Tutorial Part 12.pdf
    2. https://github.com/rabbibotton/clog/blob/main/LEARN.md
    3. Various Common Lisp textbooks and online resources (implicitly referenced in the tutorial)

    代码背景:

    • 在Common Lisp中,宏(macro)是一种非常强大的编程工具。与函数不同,宏在编译时就会展开代码。
    • let用于定义局部变量,push用于向列表前端插入元素,reverse用于将列表反转。

    代码分析:

    (defmacro example ()
      (let ((stack nil))
        (push 'print stack)
        (push "hello world" stack)
        (reverse stack)))

    第1行:(defmacro example ()

    • 作用:使用defmacro定义一个名为example的宏。宏的作用是在编译时生成代码,而不是在运行时执行。
    • 重点:与普通函数不同,宏在编译时被调用,返回的不是值,而是代码片段(即Lisp表),这些代码会替换宏调用的位置。

    第2行:(let ((stack nil))

    • 作用:使用let在宏定义体中声明一个局部变量stack,初始值为nilstack是一个临时的空列表,将在后续操作中存储多个元素。
    • 重点let在Lisp中用于创建局部绑定,可以在后续代码中修改这些局部变量。

    第3行:(push 'print stack)

    • 作用:使用push将符号'print推入到stack的前端push将元素插入到列表的开头,并返回更新后的列表。
    • 解释:此时,stack的内容是(print)push是一个破坏性操作,它直接修改stack的值。

    第4行:(push "hello world" stack)

    • 作用:将字符串"hello world"压入stack的前端。现在,stack的内容变为("hello world" print)
    • 解释push的机制是将新元素插入到表头,所以元素的顺序是反向的,先插入的元素在后面。

    第5行:(reverse stack)

    • 作用:对stack进行反转操作,返回一个新的列表。此时,stack的内容为(print "hello world")
    • 解释:由于push插入元素的顺序是从前往后的,反转操作保证了最终的顺序是我们期望的顺序。

    宏调用分析:

    当我们调用(example)时,宏的展开和生成过程如下:

    1. example被调用时,它并不会立即执行代码,而是生成一段Lisp代码。
    2. 通过let定义的局部变量stack,我们逐步向其添加元素(首先是符号'print,接着是字符串"hello world")。
    3. 最后,宏返回的是反转后的stack,即(print "hello world")

    因此,宏展开后的结果是:

    (print "hello world")

    换句话说,当我们编写(example)时,编译器实际上将其替换为:

    (print "hello world")

    难点和要点
    解析:

    • 宏与函数的区别
    • 函数在运行时执行,而宏在编译时展开。宏返回的不是值,而是代码片段(Lisp列表
    • 因此,宏的优势在于可以生成复杂的代码结构,并延迟执行某些计算或操作。
    • push和列表操作
    • push是Lisp中一个常用的操作,用于将元素插入到列表的前端。它是破坏性的,因为它修改了原列表的结构。
    • 需要注意的是,push插入的顺序是从前往后,因此我们需要通过reverse来调整顺序。
    • 宏的返回值
    • 宏的返回值是一个Lisp列表,这个列表在调用宏的地方会作为代码执行。在这个例子中,返回值是(print "hello world"),这意味着宏调用的位置会被这段代码替换。

    示例:

    调用(example)之后,等价于执行如下代码:

    (print "hello world")

    输出结果为:

    hello world

    总结:

    • 这段代码展示了宏的基本用法。通过宏,您能够在编译时生成代码,而不仅仅是像函数那样在运行时返回值。
    • push的操作顺序和reverse的搭配使用是这里需要特别注意的点。
    • 这种宏可以用于生成模板化的代码片段,极大提高代码的可复用性和灵活性。

  • 🎩 面向对象的艺术:Common Lisp中的结构和类


    🌱 引言:模块化系统与面向对象编程

    在编程的世界里,设计大规模系统的关键在于创造具有清晰接口和协议的模块化单元,而不是死板地局限于模块的具体实现方式。许多编程语言都依赖于 面向对象编程(OOP)来实现这种模块化的愿景。然而,随着时间推移,语言设计者逐渐意识到,单靠面向对象的模型并不足以应对复杂的编程需求。于是,模板(template)、命名空间(namespace)等概念应运而生。

    而在这个过程中,许多语言却由于其静态特性和标准化的限制,无法灵活地扩展其对象模型。要想改进或增强这些模型,往往意味着要创造一门新的语言。痛苦!但对于 Common Lisp 来说,这种痛苦不存在。它一直以来都是一门 “可编程的语言”,并且在 CLOS(Common Lisp Object System) 标准化之前,Lisp 就有各种自定义的对象实现。

    Common Lisp 的独特之处在于,它将面向对象编程的各个部分(如 继承封装多态数据抽象)拆解开来,使你可以根据具体问题的领域来灵活组合这些元素,而不是强行套用某种固定的解决方案。正如 Imagine Dragons 所唱的那样:“你让我崩溃,但又重塑了我,成为一个真正的信徒。”——Common Lisp 让我们摆脱了传统对象模型的束缚,给我们带来了新的信仰。

    🏗️ 结构体的老路:defstruct 的基础

    🧱 结构体的基本构成

    早期的 Lisp 开发者是通过 抽象数据类型(ADT)结构体(struct) 来实现对象的概念。在 Common Lisp 中,这个概念通过 defstruct 实现。defstruct 是一种非常简单的方式,用于定义一个数据结构,它的主要优点之一就是 快速

    例如,以下是一个简单的结构体定义:

    (defstruct my-record
      "一个示例结构体"
      first-name
      last-name)

    这个宏做了几件重要的事情:

    1. 定义了一个包含两个槽(slot)的聚合数据结构,分别是 first-namelast-name
    2. 提供了一个构造函数 make-my-record,可以通过关键词参数来初始化槽的值。例如:
    (defvar an-instance nil)  ; 定义全局变量  
    (setf an-instance (make-my-record :first-name "David" :last-name "Botton"))
    1. 提供了一个复制构造函数 copy-my-record,用于创建现有结构体实例的副本。
    2. 为每个槽自动生成了访问器函数。例如,(my-record-first-name an-instance) 返回 first-name 槽的值,而 (setf (my-record-first-name an-instance) "New Name") 则设置该槽的值。
    3. 将结构体加入类型层次中,例如 (typep an-instance 'my-record) 返回 T

    ⏳ 结构体的局限

    虽然 defstruct 提供了一种轻量级的方式来定义数据结构,但它的功能相对有限。如果你需要更复杂的功能,例如多重继承、动态分发等,你可能更适合使用 defclassdefstruct 的一些优势(如速度)在现代编译器的优化下也变得不太重要了。

    因此,虽然 defstruct 是 Lisp 历史中的一部分,但许多现代应用场景更倾向于使用 defclass 来实现更复杂的对象行为。

    🏎️ 面向对象的进阶:defclass 的力量

    ⚙️ 类的基本构成

    defclass 是 Common Lisp 中用于定义类的宏,它提供了更多的灵活性和功能。让我们从最简单的类定义开始:

    (defclass my-record ()
      ((first-name
        :accessor first-name
        :initarg :first-name
        :initform "")
       (last-name
        :accessor last-name
        :initarg :last-name
        :initform "")))

    这个类定义做了几件事:

    1. 定义了一个包含两个槽的聚合数据结构。
    2. 提供了一个构造函数 make-instance,允许通过关键词参数初始化槽的值。
    3. 没有生成复制构造函数,因为对象的复制可能涉及复杂的语义。
    4. 为每个槽生成了访问器函数,允许安全地访问和修改槽的值。
    5. 将类加入类型层次中,例如 (typep an-instance 'my-record) 返回 T

    🧬 继承和多重继承

    defclass 的一个强大功能是支持多重继承。我们可以创建一个继承自多个父类的子类:

    (defclass phone-record ()
      ((phone
        :accessor phone)))
    
    (defclass employee-record (my-record phone-record)
      ((title
        :accessor title)))

    在这个例子中,employee-record 继承了 my-recordphone-record,因此它拥有四个槽:first-namelast-namephonetitle。通过这种方式,我们可以轻松地将多个类的功能组合到一个子类中。

    🔧 方法与多态

    在面向对象编程中,方法是与对象交互的主要方式。在 Common Lisp 中,方法通过 defmethod 定义。与其他语言不同的是,Common Lisp 提供了 多重分发 的能力,这意味着方法不仅可以根据一个对象的类型来选择执行的代码,还可以根据多个对象的类型进行选择。

    例如,我们可以定义一个方法来设置员工的职位和电话号码:

    (defmethod setup-new-employee ((employee employee-record) title phone)
      (setf (title employee) title)
      (setf (phone employee) phone))

    然后我们可以像这样使用这个方法:

    (setf an-instance (make-instance 'employee-record :first-name "George" :last-name "Forist"))
    (setup-new-employee an-instance "Boss Man" "954-555-1212")

    通过这种方式,我们可以根据不同的对象类型定义不同的行为,实现多态。

    🎭 封装与泛型函数:defgeneric 的魔力

    在 Common Lisp 中,封装通常通过 包(package) 来实现。你可以将类和方法封装在一个包中,限制外部代码对这些符号的访问,这样就实现了封装的概念。

    此外,Common Lisp 还提供了 泛型函数(generic function),它允许你定义一个函数,但具体执行哪个方法则取决于传入的参数类型。例如:

    (defgeneric setup-new-employee (employee-object title phone))

    泛型函数通过动态分发的机制,确保在运行时根据对象的类型调用相应的方法。

    🧑‍💻 结语:Lisp,面向对象的信徒

    通过这篇文章,我们探索了 Common Lisp 中结构体和类的基本概念。虽然 defstruct 提供了一个简单快捷的方式来定义数据结构,但 defclass 及其相关的 defmethoddefgeneric 提供了更强大的功能,尤其是在多重继承和多态性方面。

    Common Lisp 的灵活性和可扩展性使得它在面向对象编程领域独树一帜,正如 Imagine Dragons 的歌词里所说的那样:“你让我崩溃,但又重塑了我,成为一个真正的信徒。”


    面向记忆的学习材料

    快速学习并记住Common Lisp中结构体和类的相关知识,包括defstruct和CLOS (Common Lisp Object System)的基本概念和用法。

    知识点: Common Lisp中的结构体(defstruct)
    题目: 在Common Lisp中,defstruct宏的主要作用是什么?
    选项:
    A. 定义函数
    B. 创建包
    C. 定义抽象数据类型
    D. 声明变量

    正确答案: C
    解析: defstruct宏用于定义抽象数据类型(ADT)或称为结构体。它创建一个聚合数据结构,包含多个槽(slots),并自动生成构造函数、复制函数和访问器。这是Common Lisp中实现面向对象编程概念的早期方式之一。
    速记提示: 记住”struct”是”structure”的缩写,意味着”结构”,因此defstruct就是定义一个结构化的数据类型。

    知识点: defstruct生成的函数
    题目: 以下哪个不是defstruct自动生成的函数?
    选项:
    A. 构造函数(make-struct-name)
    B. 复制函数(copy-struct-name)
    C. 访问器(struct-name-element)
    D. 删除函数(delete-struct-name)

    正确答案: D
    解析: defstruct自动生成构造函数(make-struct-name)、复制函数(copy-struct-name)和每个成员的访问器(struct-name-element)。它不会生成删除函数,因为Lisp使用垃圾回收机制自动管理内存,不需要显式删除对象。
    速记提示: 记住CCC – Create(构造), Copy(复制), Component access(成员访问)。

    知识点: defstruct的封装性
    题目: 在Common Lisp中,defstruct如何实现封装?
    选项:
    A. 通过内部方法
    B. 通过私有变量
    C. 通过包系统
    D. defstruct本身不提供封装

    正确答案: C
    解析: defstruct本身不直接提供封装机制。在Common Lisp中,封装通常通过包系统(package system)实现。将defstruct定义放在单独的包中,可以控制哪些部分对外可见,从而实现封装。
    速记提示: 想象将结构体”包装”在一个包里,实现封装。

    知识点: Common Lisp对象系统(CLOS)
    题目: CLOS代表什么?
    选项:
    A. Common Lisp Object Syntax
    B. Common Lisp Object System
    C. Common Lisp Oriented Structure
    D. Common Lisp Object Standard

    正确答案: B
    解析: CLOS代表Common Lisp Object System,是Common Lisp的标准化面向对象编程系统。它提供了更强大和灵活的对象定义和方法调度机制,包括多重继承和多重方法分派。
    速记提示: 记住CLOS中的O代表Object(对象),S代表System(系统),强调它是一个完整的对象系统。

    知识点: defclass基本语法
    题目: 在defclass定义中,以下哪个不是槽(slot)选项?
    选项:
    A. :accessor
    B. :initarg
    C. :initform
    D. :inherit

    正确答案: D
    解析: defclass中常用的槽选项包括:accessor(定义访问器),:initarg(允许通过make-instance初始化),:initform(定义默认值)。:inherit不是有效的槽选项,继承是通过类定义的参数列表实现的。
    速记提示: 记住AII – Accessor, Initarg, Initform是三个主要的槽选项。

    知识点: defclass vs defstruct
    题目: 相比defstruct,defclass的主要优势是什么?
    选项:
    A. 更快的执行速度
    B. 自动生成复制构造函数
    C. 支持多重继承
    D. 更简单的语法

    正确答案: C
    解析: defclass相比defstruct的主要优势是支持多重继承。此外,它还提供了更细粒度的控制,如自定义构造函数和访问器,以及动态方法分派。虽然defstruct在某些情况下可能有轻微的性能优势,但defclass的功能更加强大和灵活。
    速记提示: class比struct更”类”似真实世界的复杂关系,因此支持多重继承。

    知识点: defmethod的作用
    题目: defmethod的主要作用是什么?
    选项:
    A. 定义新的类
    B. 创建泛型函数
    C. 为特定类定义方法
    D. 实现多重继承

    正确答案: C
    解析: defmethod用于为特定类或类的组合定义方法。它允许将函数与类直接关联,实现了面向对象编程中的多态性。defmethod通过指定参数的类型来限制方法的适用范围。
    速记提示: method是方法,defmethod就是定义特定类的方法。

    知识点: 泛型函数(Generic Functions)
    题目: 在Common Lisp中,泛型函数的主要特点是什么?
    选项:
    A. 只能接受一个参数
    B. 根据参数类型动态分派到适当的方法
    C. 必须使用defgeneric显式定义
    D. 不支持多重分派

    正确答案: B
    解析: 泛型函数的主要特点是能够根据参数的类型动态分派到适当的方法。这实现了运行时的多态性。泛型函数可以接受多个参数,支持多重分派,并且不一定要显式使用defgeneric定义(尽管显式定义有其好处)。
    速记提示: 想象泛型函数如同一个智能路由器,根据”参数类型”这个地址,将调用分派到正确的方法。

    知识点: defgeneric的用途
    题目: 使用defgeneric显式定义泛型函数的主要好处是什么?
    选项:
    A. 提高程序执行速度
    B. 自动生成文档
    C. 定义清晰的接口和协议
    D. 简化方法定义过程

    正确答案: C
    解析: 虽然不是必须的,但使用defgeneric显式定义泛型函数的主要好处是可以定义清晰的接口和协议。它帮助开发者明确指定函数的预期用法,包括参数数量和可能的类型。这对于创建大型系统和库特别有用,因为它提高了代码的可读性和可维护性。
    速记提示: generic意味着”通用的”,defgeneric就像是定义一个通用的接口或协议。

    知识点: CLOS中的多重继承
    题目: 在CLOS中,如何实现多重继承?
    选项:
    A. 使用多个defstruct定义
    B. 在defclass的参数列表中列出多个父类
    C. 使用special关键字
    D. 通过defmethod实现

    正确答案: B
    解析: 在CLOS中,多重继承是通过在defclass的参数列表中列出多个父类来实现的。例如:(defclass employee-record (my-record phone-record) …)。这允许一个类继承多个父类的属性和方法,提供了强大的代码重用机制。
    速记提示: 想象defclass的参数列表如同一个”家谱”,列出了所有的”父辈”。

    知识点: CLOS中的方法组合
    题目: CLOS中的:after,:before,:around方法修饰符的作用是什么?
    选项:
    A. 定义方法的执行顺序
    B. 指定方法的访问权限
    C. 设置方法的优先级
    D. 声明方法的返回类型

    正确答案: A
    解析: CLOS中的:after,:before,:around方法修饰符用于定义方法的执行顺序。:before方法在主方法之前执行,:after方法在主方法之后执行,:around方法可以包围主方法的执行。这种机制提供了强大的方法组合能力,允许在不修改原有方法的情况下扩展或修改行为。
    速记提示: 想象这些修饰符如同舞台表演,:before是开场,:after是谢幕,:around是整场演出的框架。

    知识点: CLOS中的槽选项
    题目: 在CLOS中,:allocation槽选项的作用是什么?
    选项:
    A. 分配内存空间
    B. 指定槽的存储位置
    C. 设置槽的初始值
    D. 定义槽的访问权限

    正确答案: B
    解析: 在CLOS中,:allocation槽选项用于指定槽的存储位置。它有两个主要值::instance(默认值,每个实例都有自己的槽)和:class(类槽,所有实例共享一个值)。这允许开发者控制数据是属于单个对象还是整个类。
    速记提示: allocation关键词暗示了”分配”的概念,这里是分配存储位置。

    知识点: CLOS中的多重分派
    题目: CLOS支持多重分派的主要机制是什么?
    选项:
    A. 使用多个defclass定义
    B. 通过defgeneric实现
    C. 在defmethod中指定多个参数的类型
    D. 使用特殊的dispatch关键字

    正确答案: C
    解析: CLOS支持多重分派的主要机制是在defmethod中指定多个参数的类型。这允许方法的选择不仅基于第一个参数(如传统的单分派OOP),而是基于多个参数的类型。这提供了更灵活的方法分派能力,特别适用于需要考虑多个对象交互的情况。
    速记提示: 想象defmethod如同一个多维坐标系,每个参数类型都是一个维度,共同决定调用哪个方法。

    知识点: CLOS中的实例初始化
    题目: 在CLOS中,如何在创建对象时初始化槽值?
    选项:
    A. 使用defvar
    B. 通过:initarg和make-instance
    C. 只能在defclass中使用:initform
    D. 必须在构造函数中手动设置

    正确答案: B
    解析: 在CLOS中,通过:initarg槽选项和make-instance函数来在创建对象时初始化槽值。在defclass中为槽定义:initarg,然后在调用make-instance时使用对应的关键字参数来设置初始值。例如:(make-instance ‘my-class :slot-name value)。这提供了灵活的对象初始化机制。
    速记提示: init在编程中通常表示”初始化”,arg表示”参数”,所以:initarg就是用于初始化的参数。

    知识点: CLOS中的方法组合顺序
    题目: 在CLOS中,标准方法组合的执行顺序是什么?
    选项:
    A. primary, :before, :after, :around
    B. :before, primary, :after, :around
    C. :around, :before, primary, :after
    D. :before, :around, primary, :after

    正确答案: C
    解析: 在CLOS的标准方法组合中,执行顺序是::around方法(如果有),然后是所有:before方法,接着是最具体的primary方法,最后是所有:after方法。:around方法有能力完全控制其他方法的执行,包括是否调用其他方法。这种灵活的组合机制允许复杂的行为定制。
    速记提示: 记住ABPA顺序 – Around, Before, Primary, After。想象:around如同舞台的帷幕,首先拉开,最后合上。

    知识点: CLOS中的槽继承
    题目: 在CLOS中,子类如何继承父类的槽?
    选项:
    A. 自动继承所有槽
    B. 只继承使用:inherit选项的槽
    C. 需要在子类中重新定义所有槽
    D. 继承槽名,但可以重新定义槽的属性

    正确答案: D
    解析: 在CLOS中,子类自动继承父类的槽名,但可以重新定义继承的槽的属性。这意味着子类可以保留父类的槽结构,同时允许修改特定槽的特性,如访问器、初始值等。这种机制提供了继承的灵活性,允许子类根据需要定制继承的行为。
    速记提示: 想象槽如同房间的布局,子类继承了房间的名字,但可以重新装修每个房间。

    知识点: CLOS中的类优先级列表
    题目: CLOS中的类优先级列表(Class Precedence List)的主要作用是什么?
    选项:
    A. 决定方法的执行顺序
    B. 控制实例的创建顺序
    C. 确定多重继承中的继承顺序
    D. 管理类的内存分配

    正确答案: C
    解析: CLOS中的类优先级列表主要用于确定多重继承中的继承顺序。当一个类有多个父类时,类优先级列表决定了在方法查找、槽访问等操作中如何解析潜在的冲突。它保证了一致的行为,特别是在复杂的继承层次结构中。CLOS使用一种称为拓扑排序的算法来计算这个列表。
    速记提示: 优先级列表就像是家族树的”重要性排序”,决定在多个”父辈”中听谁的。

    知识点: CLOS中的元对象协议(MOP)
    题目: CLOS的元对象协议(MOP)主要提供什么功能?
    选项:
    A. 自动生成文档
    B. 优化代码执行效率
    C. 自定义和扩展CLOS本身
    D. 简化类定义语法

    正确答案: C
    解析: CLOS的元对象协议(MOP)主要提供了自定义和扩展CLOS本身的能力。MOP允许程序员修改类、泛型函数、方法等的行为,甚至可以改变它们的实现方式。这使得CLOS成为一个高度可扩展的对象系统,能够适应各种特殊需求和编程范式。
    速记提示: 元(Meta)意味着”关于自身的”,所以元对象协议就是允许你改变对象系统本身的规则。

    知识点: CLOS中的实例更新
    题目: 在CLOS中,如何更新现有实例以适应类定义的变化?
    选项:
    A. 自动更新,无需操作
    B. 使用update-instance-for-redefined-class函数
    C. 必须创建新实例替换旧实例
    D. 通过重新加载整个系统

    正确答案: B
    解析: 在CLOS中,当类定义发生变化时,可以使用update-instance-for-redefined-class函数来更新现有实例。这个函数允许开发者定义如何处理旧实例,包括如何初始化新增的槽、如何处理被移除的槽等。这提供了动态更新对象的强大机制,特别适用于长时间运行的系统。
    速记提示: update暗示更新,redefined暗示重新定义,结合起来就是”为重新定义的类更新实例”。

    知识点: CLOS中的方法组合类型
    题目: 除了标准方法组合外,CLOS还内置支持哪些方法组合类型?
    选项:
    A. sum, max, min
    B. and, or, progn
    C. first, last, random
    D. add, subtract, multiply

    正确答案: B
    解析: 除了标准方法组合,CLOS还内置支持and, or, progn等方法组合类型。这些组合类型改变了primary方法的组合方式:and组合在遇到nil返回值时停止并返回nil,or组合在遇到非nil值时停止并返回该值,progn组合执行所有方法并返回最后一个方法的值。这些组合类型提供了强大的方法组合灵活性。
    速记提示: and, or, progn都是Lisp中常见的控制结构,CLOS将它们扩展到了方法组合中。

    总结

    本学习材料涵盖了Common Lisp中结构体(defstruct)和类(CLOS)的核心概念,包括:

    1. defstruct的基本用法和生成的函数
    2. CLOS的主要组成部分:defclass, defmethod, defgeneric
    3. 类的定义、继承和槽选项
    4. 方法的定义、组合和多重分派
    5. 泛型函数和动态分派机制
    6. CLOS的高级特性,如元对象协议(MOP)和实例更新

    通过掌握这些概念,学习者应该能够理解Common Lisp中面向对象编程的基本原理和高级特性,为进一步学习和应用打下坚实基础。

    参考文献

    1. Common Lisp – The Tutorial Part 11
    2. ANSI Common Lisp by Paul Graham
    3. Object-Oriented Programming in Common Lisp: A Programmer’s Guide to CLOS by Sonya E. Keene

    CLOS(Common Lisp Object System)中的:after:before:around方法修饰符用于定义方法的执行顺序,因此正确答案是:

    A. 定义方法的执行顺序

    详细解释:

    在CLOS中,方法组合(Method Combination)允许你通过一些修饰符来控制在调用一个泛型函数时,多个方法的执行顺序和逻辑。这些修饰符主要有以下几种:

    1. :before 方法修饰符

    • 作用:定义在主方法(primary method)之前执行的方法。
    • 特点:before方法会在主方法执行之前被自动调用,但它的返回值会被忽略,真正的返回值来自主方法或其他组合方法。

    示例

    (defmethod example :before ((obj my-class))
      (format t "Before method executed.~%"))

    在调用example这个泛型函数时,定义的:before方法会在所有的主方法之前执行。

    2. :after 方法修饰符

    • 作用:定义在主方法(primary method)之后执行的方法。
    • 特点:after方法会在主方法执行之后自动调用,且同样不会影响主方法的返回值。

    示例

    (defmethod example :after ((obj my-class))
      (format t "After method executed.~%"))

    当调用example时,:after方法会在主方法执行之后调用。

    3. :around 方法修饰符

    • 作用:定义一个包裹主方法执行的行为,通常用来执行一些前后逻辑或完全控制执行流程。
    • 特点:around方法有最高的优先级,它可以选择是否调用主方法。如果不显式调用call-next-method,主方法将不会被执行。

    示例

    (defmethod example :around ((obj my-class))
      (format t "Around method before.~%")
      (call-next-method) ; 显式调用下一个方法
      (format t "Around method after.~%"))

    在调用example时,这个:around方法会首先执行,且可以选择是否调用call-next-method来继续执行主方法或其他组合方法。

    重点难点讲解:

    1. 执行顺序
    • :before方法:先执行,且不影响主方法的返回值。
    • 主方法:核心业务逻辑,通常返回最终结果。
    • :after方法:后执行,也不影响主方法的返回值。
    • :around方法:可以完全包裹主方法的执行,甚至可以选择不执行主方法。
    1. call-next-method
    • :around方法中,call-next-method的调用非常关键。如果不调用它,主方法和其他方法组合将不会被执行。
    1. 实际应用
    • 使用:before:after来实现一些前置和后置逻辑,如日志记录、资源初始化或清理。
    • 使用:around来实现更复杂的控制逻辑,如对方法调用进行拦截或条件执行。

    总结:

    CLOS中的:before:after:around方法修饰符主要用于定义方法的执行顺序,属于方法组合的一部分,允许开发者通过这些修饰符进行更灵活的控制。这也是CLOS强大之处之一,使得它可以支持复杂的面向对象编程模式。

    因此,正确答案是 A. 定义方法的执行顺序


    “主方法”指的是普通的、未带修饰符的方法,它在方法组合中通常作为核心的功能实现。与这些修饰符(:before:after:around)相比,主方法是最重要的,因为它通常包含了实际的业务逻辑。

    接下来,我将通过一个完整的CLOS例子,展示如何使用主方法和这三个修饰符。

    例子:银行账户的存款操作

    我们通过一个简单的银行账户存款操作来说明主方法和不同修饰符的作用。

    1. 定义一个简单的类 bank-account

    (defclass bank-account ()
      ((balance :initform 0 :accessor balance)))

    这里,bank-account类有一个属性balance,表示账户余额。我们使用balance来读取或更新账户余额。

    2. 定义主方法 deposit

    (defmethod deposit ((account bank-account) amount)
      (incf (balance account) amount)
      (format t "Deposited ~d. New balance: ~d~%" amount (balance account)))

    这是一个主方法,deposit方法接受两个参数:一个是bank-account对象,另一个是存款金额amount。它的作用是增加账户余额,并输出存款成功后的新余额。

    3. 使用 :before 方法修饰符:

    (defmethod deposit :before ((account bank-account) amount)
      (when (<= amount 0)
        (error "Deposit amount must be positive!")))

    这里,我们使用:before修饰符实现一个前置检查,确保存款金额是正数。如果金额为0或负数,抛出一个错误。在主方法执行之前,这个:before方法会被调用。

    4. 使用 :after 方法修饰符:

    (defmethod deposit :after ((account bank-account) amount)
      (format t "Transaction completed successfully.~%"))

    :after修饰符定义的方法会在主方法执行完毕后被调用。这里我们在存款成功后输出一条消息,表示交易完成。

    5. 使用 :around 方法修饰符:

    (defmethod deposit :around ((account bank-account) amount)
      (format t "Starting deposit process...~%")
      (call-next-method)  ;; 执行主方法
      (format t "Deposit process finished.~%"))

    :around修饰符定义的方法会包裹主方法的执行。在这个例子中,我们在存款操作开始和结束时分别输出一些调试信息。call-next-method用于调用主方法及其相关的:before:after方法。如果我们不调用call-next-method,主方法和其他组合方法将不会被执行。

    完整代码:

    (defclass bank-account ()
      ((balance :initform 0 :accessor balance)))
    
    (defmethod deposit ((account bank-account) amount)
      (incf (balance account) amount)
      (format t "Deposited ~d. New balance: ~d~%" amount (balance account)))
    
    (defmethod deposit :before ((account bank-account) amount)
      (when (<= amount 0)
        (error "Deposit amount must be positive!")))
    
    (defmethod deposit :after ((account bank-account) amount)
      (format t "Transaction completed successfully.~%"))
    
    (defmethod deposit :around ((account bank-account) amount)
      (format t "Starting deposit process...~%")
      (call-next-method)  ;; 执行主方法
      (format t "Deposit process finished.~%"))

    执行效果:

    假设我们创建一个银行账户并执行几次存款操作:

    ;; 创建账户对象
    (setq my-account (make-instance 'bank-account))
    
    ;; 正常存款操作
    (deposit my-account 100)

    输出结果

    Starting deposit process...
    Deposited 100. New balance: 100
    Transaction completed successfully.
    Deposit process finished.

    执行流程:

    1. :around方法首先被调用,用于输出“Starting deposit process…”。
    2. :before方法接着执行,检查存款金额是否为正数。
    3. 主方法执行,增加账户余额并输出存款成功后的信息。
    4. :after方法执行,输出“Transaction completed successfully.”。
    5. :around方法最后执行剩余部分,输出“Deposit process finished.”。

    如果我们尝试存入一个负数:

    (deposit my-account -50)

    输出结果

    Error: Deposit amount must be positive!

    由于:before方法在主方法之前执行,它阻止了非法的存款操作。

    详细说明:

    1. :before的作用

    • 主要用于在主方法执行前做一些预处理或检查。它不会改变主方法的返回值,也不能阻止主方法的执行,除非显式抛出错误或其他异常。

    2. :after的作用

    • 它在主方法执行后运行,通常用于一些后置操作,如日志记录或资源清理。无论主方法返回什么,:after方法的返回值都不会影响主方法的返回结果。

    3. :around的作用

    • 它最灵活,可以包裹整个方法调用,并且有权决定是否调用call-next-method来继续执行主方法及其他组合方法。如果不调用call-next-method,主方法和其他组合方法就不会被执行。
    • 适合用来实现事务的开始或结束、条件性执行等复杂逻辑。

    总结:

    • 主方法:是实现核心功能的普通方法。
    • :before方法:在主方法之前执行,适合做前置检查或初始化工作。
    • :after方法:在主方法之后执行,适合做后续处理,比如日志或清理工作。
    • :around方法:包裹整个方法调用,可以完全控制执行流程,甚至可以选择不调用主方法。

    CLOS(Common Lisp Object System)支持多重分派的主要机制是通过在defmethod中指定多个参数的类型。这种方式允许方法根据多个参数的类型来确定哪一个具体的方法应该被调用,这正是多重分派的概念。

    详细解释:

    多重分派(Multiple Dispatch)意味着当调用一个通用函数(generic function)时,系统会根据多个参数的类型来选择合适的方法。在CLOS中,这通过方法组合(method combination)和基于参数类型的方法特化(method specialization)来实现。

    CLOS的多重分派机制:

    1. defgeneric定义通用函数
      通用函数(generic function)是一个抽象的函数签名,可以有多个方法(defmethod)实现。每个方法可以为不同的参数类型进行特化。
    2. defmethod定义具体方法
      在定义方法时,可以给每个参数指定特定的类型,这样CLOS可以根据调用时传入的参数类型选择最合适的方法。
    3. 多重分派
      当调用一个通用函数时,CLOS会根据所有参数的类型,选择与这些参数匹配的最具体的实现。这就是多重分派的核心:不仅根据第一个参数类型,还根据其他参数的类型来分派到合适的方法

    代码示例:

    ;; 定义两个类
    (defclass animal () ())
    (defclass dog (animal) ())
    
    ;; 定义一个通用函数 `interact`
    (defgeneric interact (x y))
    
    ;; 根据参数的类型定义不同的实现
    (defmethod interact ((x animal) (y animal))
      (format t "Animal interacts with another animal.~%"))
    
    (defmethod interact ((x dog) (y animal))
      (format t "Dog interacts with an animal.~%"))
    
    (defmethod interact ((x dog) (y dog))
      (format t "Dog interacts with another dog.~%"))
    
    ;; 调用不同的情况
    (let ((rex (make-instance 'dog))
          (generic-animal (make-instance 'animal)))
      (interact rex generic-animal)  ;; 输出: Dog interacts with an animal.
      (interact rex rex))            ;; 输出: Dog interacts with another dog.

    解析:

    其他选项
    解析:

    总结:

    CLOS中的多重分派主要通过在defmethod中为参数指定类型来实现。系统根据调用时参数的类型来选择合适的方法,因此选项 C 是正确的。


    在CLOS(Common Lisp Object System)中,创建对象时初始化槽值的主要机制是通过:initargmake-instance的组合。这允许在使用make-instance创建对象时,传递初始化参数来设置对象的槽值。

    详细解释:

    CLOS中的实例初始化:

    1. defclass定义类和槽
      在CLOS中,类的定义使用defclass,其中一个类包含若干“槽”(slots)。槽是对象的属性,它们可以在创建对象时通过初始化参数进行赋值。
    2. :initarg指定初始化参数
      defclass中定义槽时,可以使用:initarg选项来指定一个关键字,这个关键字用于在创建对象时传递值给该槽。
    3. make-instance创建对象
      创建对象时使用的是make-instance函数。你可以通过make-instance传递与:initarg匹配的关键字参数来初始化槽。

    示例代码:

    ;; 定义一个类 person,包含两个槽:name 和 age
    (defclass person ()
      ((name :initarg :name    ;; 使用 :initarg :name 允许通过 :name 关键字初始化
             :initform "Unknown"  ;; 默认值为 "Unknown"
             :accessor person-name)  ;; 定义读取器方法
       (age :initarg :age        ;; 使用 :initarg :age 允许通过 :age 关键字初始化
            :initform 0          ;; 默认值为 0
            :accessor person-age)))  ;; 定义读取器方法
    
    ;; 使用 make-instance 创建对象,并通过 :initarg 传递初始值
    (let ((p (make-instance 'person :name "Alice" :age 30)))
      (format t "Name: ~a, Age: ~a~%" (person-name p) (person-age p)))

    解析:

      Name: Alice, Age: 30

    其他选项
    解析:

    总结:

    在CLOS中,创建对象时初始化槽值的推荐方法是通过:initargmake-instance

  • 🚦 跟随Lisp地图:一场函数映射的奇幻之旅


    🚀 引言:从排队到映射

    从我们小时候开始,就被教导要排队:排队买东西、排队进场、排队离场,甚至排队做火灾演习。无论我们是否喜欢, “排队” 都是高效处理信息的方式。而在编程世界里,序列(sequence) 就像排队一样,是整理和处理数据的常见方式。

    但你有没有想过,如果我们能对每个人进行某种变换,比如给每个人发一顶帽子,或者给每个人贴一个标签?这就是函数映射(mapping) 的精髓:给序列中的每个元素应用一个函数,然后返回一个新序列。Common Lisp中的map函数,正是这张带你探索序列的“地图”。

    🗺️ 函数映射:让Lisp带你游览序列

    在Lisp中,map函数是一个多才多艺的导游。它可以带你走遍各种序列,还能灵活地帮你为每个元素进行“个性化处理”。就像《Maroon 5》的歌词那样,map函数总会将你带到目的地。

    (map 'list (lambda (item) (format nil "'~A'" item)) '(a b c))
    => ("'A'" "'B'" "'C'")

    在上面的例子中,map函数就像一个导游,它带着我们走过'(a b c)这个列表,并对每个元素应用了一个lambda函数,最终返回了一个新的列表。这个过程就像是给每个元素打了个标签,然后将打了标签的元素整齐地排成一排,回馈给你。

    (map result-type function sequence)

    🧳 不同的行李箱:序列类型的选择

    有意思的是,map函数不仅限于列表,它还能带你游览不同的数据结构。比如,你可以让它帮你打包成一个向量(vector):

    (map 'vector (lambda (item) (format nil "'~A'" item)) '(a b c))
    => #("'A'" "'B'" "'C'")

    这就像是导游问你:“你喜欢用背包还是行李箱呢?” 不管选择哪种方式,map函数都会帮你把行李整理得井井有条。

    🎯 多个序列:并行处理的妙招

    有时候,我们的任务不止一个序列。比如说,你同时有一列名字和一列编号,想要把它们合在一起。这时,map函数可以同时处理多个序列,并且会根据最短的序列来决定遍历的次数:

    (map 'list (lambda (item1 item2) (format nil "'~A' ~A" item1 item2)) '(a b c) #(1 2 3 4))
    => ("'A' 1" "'B' 2" "'C' 3")

    这就像是有两队人在排队,map导游会让每队的第一个人同时上车,直到其中一队的人走完为止。最后,你得到的结果是每对人都被一一匹配并处理了。

    🗑️ map的轻装上阵:返回类型为nil

    有时,导游并不关心结果,只想走个流程。这时候你可以用nil作为返回类型,表示“只管做,不管结果”。这就是Lisp中的map函数返回nil时的表现:

    (map 'nil (lambda (item) (format nil "'~A'" item)) '(a b c))
    => nil

    这就像导游带你游览了一圈,虽然你看到了所有景点,但你并没有带回任何纪念品。

    🐢 深入列表的秘密:mapcar、maplist的魔法

    除了map,Lisp里还有几位更为专注的导游,比如mapcarmaplist。它们在处理列表时有各自的独特风格:

    • mapcar:每次迭代时处理列表的第一个元素(即car)。
      (mapcar (lambda (item) (format nil "'~A'" item)) '(a b c))
      => ("'A'" "'B'" "'C'")

    就像是在每个景点挑选最有代表性的部分来讲解,mapcar只会关注当前元素。

    • maplist:每次迭代时处理整个子列表(即cdr)。
      (maplist (lambda (item) (format nil "'~A'" item)) '(a b c))
      => ("'(A B C. '" "'(B C)'" "'(C)'")

    这就好比导游不仅告诉你某个景点的故事,还会带你了解接下来所有景点的背景。

    🧩 寻找宝藏:find与position

    除了 map 系列导游,Lisp 还有一位擅长寻宝的“侦探”:find。它能帮助你在序列中找到某个特定元素,就像在沙漠中寻找宝藏一样:

    (find 3 '(1 2 3 4))
    => 3

    find-iffind-if-not 则可以使用谓词函数进行条件搜索:

    (find-if #'oddp '(2 4 3 5))
    => 3

    如果你更关心宝藏的位置而不是它本身,那么position函数就是你的不二选择:

    (position 3 '(1 2 3 4))
    => 2

    这就像是在地图上标记出宝藏的确切位置,从而为之后的行动做好准备。

    📊 数据可视化:使用Mermaid展示映射逻辑

    为了更好地理解map函数的工作原理,我们可以用Mermaid图表来可视化它的映射过程。下图展示了如何将一个函数应用到每个序列元素上,并将结果返回:

    graph TD
        A[输入序列] -->|应用函数| B[处理序列]
        B --> C[输出序列]

    这个过程简单却强大,正是map函数的核心功能。

    🎉 结论:Lisp的映射世界

    正如我们一路游览的那样,Lisp中的map函数及其家族成员为我们提供了一种灵活而高效的方式来处理序列。不管你是想对每个元素打标签,还是想同时处理多个序列,map都能提供帮助。而findposition这样的函数则让我们在序列中找到特定的“宝藏”,无论是值还是位置。

    所以,下次当你面对一大堆排队等候的数据时,记得叫上Lisp的“导游”们,它们会带你顺利完成这场奇妙的映射之旅。


    面向记忆的学习材料

    快速学习并记住Common Lisp中关于映射(Mapping)和查找(Find)函数的内容。

    知识点: map函数的基本用法
    题目: 在Common Lisp中,map函数的主要作用是什么?
    选项:
    A. 对列表进行排序
    B. 在序列中查找元素
    C. 对序列中的每个元素应用一个函数
    D. 创建新的序列类型

    正确答案: C
    解析: map函数在Common Lisp中用于对序列(如列表或向量)中的每个元素应用一个指定的函数。它可以处理一个或多个序列,并根据指定的返回类型收集结果。
    速记提示: “MAP”是”Make Applied Processing”的缩写,意味着对每个元素进行应用处理。

    知识点: map函数的返回类型
    题目: 在使用map函数时,如何指定返回结果的类型?
    选项:
    A. 通过函数名指定
    B. 通过第一个参数指定
    C. 通过最后一个参数指定
    D. 不需要指定,自动推断

    正确答案: B
    解析: 在使用map函数时,第一个参数用于指定返回结果的类型。例如,’list表示返回列表,’vector表示返回向量。如果不需要收集结果,可以使用nil。
    速记提示: “First for Format”,第一个参数决定格式(返回类型)。

    知识点: map函数处理多个序列
    题目: 当map函数处理多个序列时,迭代次数如何确定?
    选项:
    A. 由第一个序列的长度决定
    B. 由最长序列的长度决定
    C. 由最短序列的长度决定
    D. 由用户指定的次数决定

    正确答案: C
    解析: 当map函数处理多个序列时,迭代次数由最短序列的长度决定。这确保了函数不会尝试访问不存在的元素。
    速记提示: “Shortest Sequence Sets Speed”,最短序列设置速度(迭代次数)。

    知识点: mapcar函数的特点
    题目: mapcar函数与map函数的主要区别是什么?
    选项:
    A. mapcar只能处理列表
    B. mapcar总是返回向量
    C. mapcar不需要指定返回类型
    D. mapcar只能处理单个元素

    正确答案: C
    解析: mapcar是专门用于处理列表的函数,它的主要特点是不需要像map那样指定返回类型。mapcar总是返回一个新的列表,其中包含应用函数后的结果。
    速记提示: “Car Carries Automatically”,car(列表的首元素)自动携带结果类型。

    知识点: maplist函数的特点
    题目: 在使用maplist函数时,每次迭代处理的是什么?
    选项:
    A. 列表的第一个元素
    B. 列表的最后一个元素
    C. 列表的所有元素
    D. 列表的剩余部分(cdr)

    正确答案: D
    解析: maplist函数在每次迭代时处理的是列表的剩余部分(cdr)。这意味着第一次迭代处理整个列表,第二次处理除第一个元素外的剩余列表,依此类推。
    速记提示: “List the Remaining”,列出剩余部分。

    知识点: mapc和mapl函数的特点
    题目: mapc和mapl函数与mapcar和maplist的主要区别是什么?
    选项:
    A. 处理的数据类型不同
    B. 返回值总是nil
    C. 不能使用lambda表达式
    D. 只能处理单个列表

    正确答案: B
    解析: mapc和mapl函数在功能上类似于mapcar和maplist,但它们的主要区别在于返回值。无论函数如何处理列表元素,mapc和mapl总是返回nil。这些函数通常用于其副作用,而不是为了收集结果。
    速记提示: “C and L lead to Nil”,C和L. mapc和mapl)导向nil。

    知识点: mapcan和mapcon函数的特点
    题目: mapcan和mapcon函数与mapcar和maplist的主要区别是什么?
    选项:
    A. 它们使用nconc收集结果
    B. 它们只能处理数字
    C. 它们返回原始列表
    D. 它们不接受自定义函数

    正确答案: A
    解析: mapcan和mapcon函数与mapcar和maplist类似,但它们使用nconc(破坏性连接)来收集结果,而不是创建新的列表。这意味着它们可以更高效地处理大量数据,但也可能修改原始数据结构。
    速记提示: “CAN and CON Concatenate”,CAN和CON(mapcan和mapcon)连接(使用nconc)。

    知识点: find函数的基本用法
    题目: find函数在Common Lisp中的主要作用是什么?
    选项:
    A. 查找序列中的特定元素
    B. 查找序列中的最大值
    C. 查找序列中的最小值
    D. 查找序列中的重复元素

    正确答案: A
    解析: find函数在Common Lisp中用于在给定的序列中查找特定的元素。如果找到了匹配的元素,它会返回该元素;如果没有找到,则返回nil。
    速记提示: “Find Finds Familiar”,find找到熟悉的(特定的元素)。

    知识点: find-if和find-if-not函数
    题目: find-if和find-if-not函数与find函数的主要区别是什么?
    选项:
    A. 它们只能用于列表
    B. 它们使用谓词函数进行测试
    C. 它们总是返回布尔值
    D. 它们查找多个元素

    正确答案: B
    解析: find-if和find-if-not函数与find函数的主要区别在于它们使用谓词函数(predicate function)来测试每个元素。谓词函数返回nil表示假,任何其他值表示真。这允许更灵活的查找条件。
    速记提示: “If for Intelligent Finding”,if用于智能查找(使用谓词函数)。

    知识点: position函数的作用
    题目: position函数在Common Lisp中的主要作用是什么?
    选项:
    A. 返回序列中元素的数量
    B. 返回序列中元素的索引位置
    C. 返回序列中的最后一个元素
    D. 返回序列中的第一个元素

    正确答案: B
    解析: position函数在Common Lisp中用于查找指定元素在序列中的索引位置。如果找到元素,它返回该元素的0基索引;如果没有找到,则返回nil。这个函数类似于find,但返回的是位置而不是元素本身。
    速记提示: “Position Points to Place”,position指向位置。

    知识点: position-if和position-if-not函数
    题目: position-if和position-if-not函数与position函数的关系是什么?
    选项:
    A. 它们返回多个位置
    B. 它们只用于向量
    C. 它们使用谓词函数进行测试
    D. 它们返回元素而不是位置

    正确答案: C
    解析: position-if和position-if-not函数与position函数的关系类似于find-if和find-if-not与find的关系。它们使用谓词函数来测试序列中的每个元素,返回第一个使谓词函数返回真(对于position-if)或假(对于position-if-not)的元素的位置。
    速记提示: “If Positions with Predicates”,if版本的position使用谓词函数。

    知识点: map函数的多序列处理
    题目: 当map函数处理多个序列时,lambda表达式的参数如何对应?
    选项:
    A. 随机对应
    B. 按序列顺序一一对应
    C. 只对应第一个序列
    D. 根据元素类型对应

    正确答案: B
    解析: 当map函数处理多个序列时,lambda表达式的参数按照序列的顺序一一对应。例如,如果有两个序列,lambda表达式应该有两个参数,第一个参数对应第一个序列的元素,第二个参数对应第二个序列的元素。
    速记提示: “Parameters Parallel Sequences”,参数与序列平行对应。

    知识点: map函数的返回值处理
    题目: 如果在map函数中指定返回类型为nil,会发生什么?
    选项:
    A. 函数会报错
    B. 返回原始序列
    C. 返回空列表
    D. 丢弃lambda表达式的结果

    正确答案: D
    解析: 当在map函数中指定返回类型为nil时,lambda表达式仍然会应用于每个元素,但其结果会被丢弃。这种用法通常用于执行副作用(如打印),而不关心收集结果。
    速记提示: “Nil Nullifies Nabbing”,nil使得抓取(结果)无效。

    知识点: find函数的返回值
    题目: 当find函数在序列中找不到指定元素时,会返回什么?
    选项:
    A. 0
    B. false
    C. nil
    D. 一个错误

    正确答案: C
    解析: 当find函数在序列中找不到指定的元素时,它会返回nil。在Common Lisp中,nil既表示”假”,也表示空列表。这个设计允许函数调用者轻松地检查是否找到了元素。
    速记提示: “Not found? Nil for Now”,没找到?现在就是nil。

    知识点: mapcar函数的列表处理
    题目: mapcar函数在处理列表时,对每个元素做什么操作?
    选项:
    A. 返回元素本身
    B. 返回元素的car部分
    C. 对元素应用指定的函数
    D. 返回元素的cdr部分

    正确答案: C
    解析: mapcar函数在处理列表时,会对列表中的每个元素应用指定的函数。这个函数可以是内置函数、自定义函数或lambda表达式。mapcar返回一个新列表,其中包含应用函数后的结果。
    速记提示: “Car Carries and Converts”,car携带并转换每个元素。

    知识点: find-if函数的使用场景
    题目: find-if函数最适合用于什么场景?
    选项:
    A. 查找特定的数值
    B. 查找满足特定条件的元素
    C. 查找列表中的最后一个元素
    D. 查找列表中的重复元素

    正确答案: B
    解析: find-if函数最适合用于查找满足特定条件的元素。它允许你提供一个谓词函数,该函数定义了你要查找的元素应满足的条件。这使得find-if非常灵活,可以用于各种复杂的查找场景。
    速记提示: “If Finds when Conditions Fit”,if在条件符合时找到元素。

    知识点: position函数的索引特性
    题目: 在Common Lisp中,position函数返回的索引从几开始?
    选项:
    A. 从-1开始
    B. 从0开始
    C. 从1开始
    D. 取决于序列类型

    正确答案: B
    解析: 在Common Lisp中,position函数返回的索引是从0开始的。这意味着序列中的第一个元素的位置是0,第二个是1,依此类推。这与许多编程语言的惯例一致,有助于在数组和其他索引结构中直接使用返回的位置。
    速记提示: “Position Points from Point Zero”,position从零点开始指向。

    知识点: map函数的通用性
    题目: 以下哪种数据结构不能直接用于map函数?
    选项:
    A. 列表
    B. 向量
    C. 字符串
    D. 哈希表

    正确答案: D
    解析: map函数可以处理任何类型的序列,包括列表、向量和字符串。然而,哈希表不是序列类型,因此不能直接用于map函数。如果需要对哈希表进行类似的操作,需要使用专门的函数如maphash。
    速记提示: “Map Manages Sequences, not Hashes”,map管理序列,不管理哈希表。

    知识点: lambda表达式在map函数中的应用
    题目: 在map函数中使用lambda表达式的主要优点是什么?
    选项:
    A. 提高程序运行速度
    B. 减少内存使用
    C. 允许定义临时的、匿名的函数
    D. 使代码更短

    正确答案: C
    解析: 在map函数中使用lambda表达式的主要优点是允许定义临时的、匿名的函数。这种方法非常灵活,可以直接在map调用中定义复杂的操作,而不需要单独定义一个命名函数。这对于一次性使用的简单操作特别有用。
    速记提示: “Lambda Lets Anonymous Actions”,lambda允许匿名操作。

    知识点: find-if-not函数的特性
    题目: find-if-not函数与find-if函数的关系是什么?
    选项:
    A. 它们是完全相同的函数
    B. find-if-not返回不满足条件的第一个元素
    C. find-if-not只用于数值比较
    D. find-if-not总是返回最后一个元素

    正确答案: B
    解析: find-if-not函数与find-if函数的关系是相反的。while find-if返回第一个使谓词函数返回true的元素,find-if-not返回第一个使谓词函数返回false的元素。这提供了一种灵活的方式来查找不满足特定条件的元素。
    速记提示: “Not Finds the First False”,not找到第一个假(不满足条件的元素)。

    总结

    本学习材料涵盖了Common Lisp中映射(Mapping)和查找(Find)函数的核心概念和用法。主要内容包括:

    1. map函数的基本用法、返回类型指定和多序列处理。
    2. 专门用于列表的映射函数:mapcar、maplist、mapc、mapl、mapcan和mapcon。
    3. 查找函数:find、find-if和find-if-not的使用方法和特点。
    4. 位置查找函数:position、position-if和position-if-not的应用。
    5. lambda表达式在这些函数中的应用。

    这些函数为处理序列和列表提供了强大而灵活的工具。map系列函数允许对序列中的每个元素应用操作,而find和position系列函数则用于在序列中查找特定元素或满足特定条件的元素。理解和灵活运用这些函数可以大大提高编程效率和代码质量。

    参考文献

    1. Common Lisp – The Tutorial Part 10.pdf
    2. https://github.com/rabbibotton/clog/blob/main/LEARN.md
    3. Common Lisp: A Gentle Introduction to Symbolic Computation by David S. Touretzky
  • 🧠 哈希表与数组:Lisp的俩位好伙伴


    “我需要一种新的药物,一种不会让我生病的药物,一种不会让我撞车或让我感觉像被压扁三英尺的药物。”
    ——Huey Lewis and the News

    虽然本教程开头引用的歌词看似和编程无关,但事实上,Lisp 的哈希表和数组系统的设计,确实能让我们像嗑药一样上瘾(纯属比喻,绝无鼓励滥用物质的意思)。在本篇文章中,我们将探索 Common Lisp 中的三位数据结构好伙伴:关联列表(a-list)属性列表(p-list)哈希表(hash table),以及数组和向量的奇妙世界。准备好了吗?让我们开始吧!

    🔗 关联列表 – 一切从 cons 开始

    关联列表(a-list)是 Lisp 中最简单的键值对存储方式之一。它由一串 cons 对象组成,而 cons 是 Lisp 中的基本构建块。每个 cons 对象都包含两个部分,分别是 car(存储键)和 cdr(存储值)。你可以将其想象为一对好朋友,一个负责记住事情,另一个负责行动。

    (defparameter *alist* (list (cons 1 2) (cons 3 4)))

    在上面的例子中,我们创建了一个简单的关联列表,包含两个键值对 (1 . 2)(3 . 4)。我们可以通过 assoc 函数快速查找键所对应的值:

    (assoc 3 *alist*) ; => (3 . 4)
    (car (assoc 3 *alist*)) ; => 3
    (cdr (assoc 3 *alist*)) ; => 4

    通过 acons,我们可以向关联列表中非破坏性地添加一对新朋友:

    (acons 5 6 *alist*) ; => '((1 . 2)(3 . 4)(5 . 6))

    不过,关联列表的一个限制是它的查找速度较慢,因为所有键值对都存储在一个线性列表中。要查找某个键值对,必须遍历整个列表,直到找到匹配的键。

    🌀 用 LOOP 显示所有的键值对

    LOOP 是 Lisp 中的强大工具,可以非常优雅地遍历列表。我们可以用它来显示关联列表中的所有键和值:

    (loop for (k . v) in *alist* do (format t “k=~A v=~A~%” k v))

    输出结果将会是:

    k=1 v=2
    k=3 v=4

    🧳 属性列表 – 有序的键值对

    属性列表(p-list)则是一种更加节省空间的键值对存储方式。它的奇数位置存储键,偶数位置存储值。这就像一对对情侣并排而坐,每个键旁边都是它的值。

    (defparameter *plist* (list 'k1 'v1 'k2 'v2 'k3 'v3))

    在这个例子中,k1k2k3 是键,而 v1v2v3 是对应的值。我们可以通过 getf 来查找键对应的值:

    (getf *plist* 'k2) ; => v2

    你还可以通过 remf 删除元素,或者通过 setf 替换某个键对应的值:

    (setf (getf *plist* 'k3) 3)

    🌀 用 LOOP 遍历属性列表

    同样,我们可以使用 LOOP 来遍历属性列表中的键值对:

    (loop for (k v) on *plist* by #’cddr do (format t “k=~A v=~A~%” k v))

    输出结果将会是:

    k=K1 v=V1
    k=K3 v=3

    🚀 哈希表 – 性能之选

    当你需要更快的查找速度时,哈希表就是你的不二选择。与关联列表和属性列表不同,哈希表使用哈希函数来快速定位键值对。它的效率远高于线性结构,尤其是当你有大量数据时。

    创建一个哈希表非常简单:

    (defparameter *hash* (make-hash-table :test #'equal))

    然后你可以向哈希表中添加键值对:

    (setf (gethash "key1" *hash*) "value1")
    (setf (gethash "key2" *hash*) "value2")

    查找键值对也非常简单:

    (gethash "key1" *hash*) ; => "value1"

    如果你不再需要某个键值对,可以用 remhash 删除它:

    (remhash "key2" *hash*)

    🌀 用 LOOP 遍历哈希表

    哈希表也可以通过 LOOP 来遍历:

    (loop for key being the hash-keys of *hash* collect key)
    (loop for value being the hash-values of *hash* do (print value))

    📊 数组与向量 – 索引式存储

    在某些情况下,你可能需要通过数字索引来访问数据,这时数组和向量将派上用场。数组可以是多维的,而向量则是单维数组。

    我们可以用 make-array 创建一个三维数组:

    (defparameter *array* (make-array '(9 9 9) :initial-element 1))

    这个数组的每个维度都包含9个元素,初始值为1。你可以通过 aref 来访问特定位置的元素:

    (aref *array* 1 2 3) ; => 1

    向量是单维数组,创建方法如下:

    (vector 1 2 3)

    访问向量元素同样使用 aref

    (aref #(1 2 3) 0) ; => 1

    🌀 用 LOOP 遍历向量

    你可以使用 LOOP 来遍历向量中的元素:

    (loop for n across #(1 2 3) do (print n))

    输出结果将会是:

    1
    2
    3

    结论

    Common Lisp 提供了多种数据结构来满足不同场景下的需求。从简单的关联列表到性能优越的哈希表,再到通过索引访问的数组和向量,每种数据结构都有其独特的优势。正如歌曲所说,我们都在寻找一种不会让我们“生病”的新工具,而 Lisp 的这些数据结构,或许就是我们在编程世界里一直寻找的“新药”。

    参考文献

    1. Common Lisp Documentation
    2. CLOG GitHub Repository – https://github.com/rabbibotton/clog

    面向记忆的学习材料

    快速学习并记住Common Lisp中哈希表和数组的重要概念和用法。

    知识点: 哈希表的创建
    题目: 在Common Lisp中,如何创建一个哈希表,其中键可以是字符串?
    选项:
    A. (make-hash-table)
    B. (make-hash-table :test #’eql)
    C. (make-hash-table :test #’equal)
    D. (create-hash-table)

    正确答案: C
    解析: 在Common Lisp中,创建哈希表使用make-hash-table函数。默认的测试函数是eql,但它不适用于字符串。要使用字符串作为键,应该使用:test #’equal选项。
    速记提示: “equal for strings” – 记住equal用于字符串键的哈希表。

    知识点: 哈希表的操作
    题目: 在Common Lisp中,如何向哈希表hash中添加键”key1″和值”value1″?
    选项:
    A. (add-to-hash hash “key1” “value1”)
    B. (setf (gethash “key1” hash) “value1”)
    C. (push “key1” “value1” hash)
    D. (insert hash “key1” “value1”)

    正确答案: B
    解析: 在Common Lisp中,向哈希表添加键值对使用setf和gethash的组合。正确的语法是(setf (gethash key hash-table) value)。
    速记提示: “setf gethash” – 记住setf和gethash的组合用于设置哈希表的值。

    知识点: 关联列表(a-list)的创建
    题目: 如何在Common Lisp中创建一个包含键值对(1 . 2)和(3 . 4)的关联列表?
    选项:
    A. ‘((1 . 2)(3 . 4))
    B. (list (cons 1 2) (cons 3 4))
    C. (make-alist ‘(1 2 3 4))
    D. (create-association-list 1 2 3 4)

    正确答案: B
    解析: 创建关联列表可以使用list函数和cons函数的组合。每个键值对用cons创建,然后用list组合成列表。注意不要使用引用的字面量形式,因为它会创建一个常量,不易修改。
    速记提示: “list of cons” – 记住关联列表是cons对象的列表。

    知识点: 关联列表的查找
    题目: 在Common Lisp中,如何在关联列表alist中查找键为3的项?
    选项:
    A. (find 3 alist)
    B. (assoc 3 alist)
    C. (gethash 3 alist)
    D. (lookup 3 alist)

    正确答案: B
    解析: 在关联列表中查找键使用assoc函数。它返回找到的第一个匹配键的整个cons单元。
    速记提示: “assoc for alist” – 记住assoc用于在关联列表中查找。

    知识点: 属性列表(p-list)的创建
    题目: 在Common Lisp中,如何创建一个包含键值对k1/v1和k2/v2的属性列表?
    选项:
    A. (make-plist ‘k1 ‘v1 ‘k2 ‘v2)
    B. ‘((k1 . v1)(k2 . v2))
    C. (list ‘k1 ‘v1 ‘k2 ‘v2)
    D. (create-property-list :k1 ‘v1 :k2 ‘v2)

    正确答案: C
    解析: 属性列表是一个普通的列表,其中奇数位置的元素是键,偶数位置的元素是值。使用list函数可以直接创建这样的列表。
    速记提示: “list of alternating keys and values” – 记住p-list是键值交替的普通列表。

    知识点: 属性列表的查找
    题目: 在Common Lisp中,如何在属性列表plist中查找键k2的值?
    选项:
    A. (assoc ‘k2 plist)
    B. (getf plist ‘k2)
    C. (gethash ‘k2 plist)
    D. (get ‘k2 plist)

    正确答案: B
    解析: 在属性列表中查找键的值使用getf函数。它直接返回与键对应的值。
    速记提示: “getf for plist” – 记住getf用于在属性列表中获取值。

    知识点: 哈希表的遍历
    题目: 在Common Lisp中,如何使用loop宏遍历哈希表hash的所有键?
    选项:
    A. (loop for key in hash collect key)
    B. (loop for key being the hash-keys of hash collect key)
    C. (loop for (key value) in hash collect key)
    D. (loop for key across hash collect key)

    正确答案: B
    解析: 使用loop宏遍历哈希表的键,需要使用”being the hash-keys of”语法。这允许直接访问哈希表的所有键。
    速记提示: “being the hash-keys” – 记住这个特殊的loop语法用于遍历哈希表的键。

    知识点: 数组的创建
    题目: 在Common Lisp中,如何创建一个3x3x3的三维数组,初始值都为1?
    选项:
    A. (make-array ‘(3 3 3) :initial-element 1)
    B. (create-3d-array 3 3 3 :fill 1)
    C. (vector 1 1 1 1 1 1 1 1 1)
    D. (array 3 3 3 1)

    正确答案: A
    解析: 使用make-array函数创建数组。第一个参数是一个列表,指定每个维度的大小。:initial-element关键字参数用于设置所有元素的初始值。
    速记提示: “make-array with dimensions list” – 记住使用列表指定维度,initial-element设置初始值。

    知识点: 数组元素的访问
    题目: 在Common Lisp中,如何访问名为array的三维数组中坐标为(1,2,3)的元素?
    选项:
    A. (array 1 2 3)
    B. (aref array 1 2 3)
    C. (get-array-element array 1 2 3)
    D. (elt array 1 2 3)

    正确答案: B
    解析: 使用aref函数访问数组的元素。第一个参数是数组,后面跟着每个维度的索引。
    速记提示: “aref for array reference” – 记住aref用于访问数组元素。

    知识点: 向量的创建
    题目: 在Common Lisp中,以下哪种方式可以创建包含元素1、2、3的向量?
    选项:
    A. (make-vector 1 2 3)
    B. (array 1 2 3)
    C. #(1 2 3)
    D. ‘(1 2 3)

    正确答案: C
    解析: 在Common Lisp中,可以使用#(…)语法快速创建向量。这是一种特殊的读取语法,直接创建一个包含指定元素的向量。
    速记提示: “#(…) for quick vector” – 记住#语法用于快速创建向量。

    知识点: 向量的遍历
    题目: 在Common Lisp中,如何使用loop宏遍历向量#(1 2 3)的所有元素?
    选项:
    A. (loop for n in #(1 2 3) do (print n))
    B. (loop for n of #(1 2 3) do (print n))
    C. (loop for n across #(1 2 3) do (print n))
    D. (loop for n being the elements of #(1 2 3) do (print n))

    正确答案: C
    解析: 使用loop宏遍历向量时,需要使用across关键字。这允许直接访问向量的所有元素。
    速记提示: “across for vector loop” – 记住across用于在loop中遍历向量。

    知识点: 关联列表的修改
    题目: 在Common Lisp中,如何修改关联列表alist中键为3的值为5?
    选项:
    A. (setf (assoc 3 alist) 5)
    B. (setf (cdr (assoc 3 alist)) 5)
    C. (setf (getf alist 3) 5)
    D. (modify-alist alist 3 5)

    正确答案: B
    解析: 要修改关联列表中某个键的值,首先使用assoc找到对应的cons单元,然后使用setf和cdr修改其值部分。
    速记提示: “setf cdr of assoc” – 记住修改alist值需要setf、cdr和assoc的组合。

    知识点: 属性列表的移除
    题目: 在Common Lisp中,如何从属性列表plist中移除键k2及其对应的值?
    选项:
    A. (remove ‘k2 plist)
    B. (delete ‘k2 plist)
    C. (remf plist ‘k2)
    D. (setf (getf plist ‘k2) nil)

    正确答案: C
    解析: 使用remf函数可以从属性列表中移除指定的键及其对应的值。remf是一个宏,它修改原始的属性列表。
    速记提示: “remf for plist removal” – 记住remf用于从属性列表中移除键值对。

    知识点: 哈希表的清空
    题目: 在Common Lisp中,如何清空哈希表hash中的所有内容?
    选项:
    A. (clear hash)
    B. (empty hash)
    C. (setf hash (make-hash-table))
    D. (clrhash hash)

    正确答案: D
    解析: 使用clrhash函数可以清空哈希表中的所有内容。这个函数会移除所有的键值对,但保留哈希表对象本身。
    速记提示: “clrhash for clear hash” – 记住clrhash用于清空哈希表。

    知识点: 符号的属性列表
    题目: 在Common Lisp中,如何获取符号some-symbol的属性列表中键some-key的值?
    选项:
    A. (get ‘some-symbol ‘some-key)
    B. (getf (symbol-plist ‘some-symbol) ‘some-key)
    C. (symbol-property ‘some-symbol ‘some-key)
    D. (plist-value ‘some-symbol ‘some-key)

    正确答案: B
    解析: 每个符号都有自己的属性列表。使用symbol-plist函数获取符号的属性列表,然后使用getf函数从中获取特定键的值。
    速记提示: “symbol-plist then getf” – 记住先用symbol-plist获取列表,再用getf获取值。

    知识点: 哈希表的测试函数
    题目: 在Common Lisp中,如果要创建一个不区分大小写的字符串键的哈希表,应使用哪个测试函数?
    选项:
    A. #’eql
    B. #’equal
    C. #’equalp
    D. #’string=

    正确答案: C
    解析: 使用#’equalp作为测试函数可以创建一个不区分大小写的字符串键的哈希表。equalp在比较字符串时会忽略大小写。
    速记提示: “equalp for case-insensitive” – 记住equalp用于创建不区分大小写的哈希表。

    知识点: 向量和数组的关系
    题目: 在Common Lisp中,向量与数组的关系是什么?
    选项:
    A. 向量和数组是完全不同的数据结构
    B. 向量是一维数组的特例
    C. 数组是向量的特例
    D. 向量和数组是同义词

    正确答案: B
    解析: 在Common Lisp中,向量是一维数组的特例。所有的向量都是数组,但不是所有的数组都是向量。向量提供了一些额外的操作符和语法糖。
    速记提示: “Vectors are 1D arrays” – 记住向量就是一维数组。

    知识点: 关联列表的反向查找
    题目: 在Common Lisp中,如何在关联列表alist中通过值2查找对应的键?
    选项:
    A. (find 2 alist :key #’cdr)
    B. (rassoc 2 alist)
    C. (assoc 2 alist)
    D. (member 2 alist :key #’cdr)

    正确答案: B
    解析: 使用rassoc函数可以在关联列表中通过值查找键。它返回第一个匹配值的整个cons单元。
    速记提示: “rassoc for reverse assoc” – 记住rassoc用于关联列表的反向查找。

    知识点: 数组元素的修改
    题目: 在Common Lisp中,如何将三维数组array中坐标为(1,2,3)的元素的值修改为5?
    选项:
    A. (setf (array 1 2 3) 5)
    B. (setf (aref array 1 2 3) 5)
    C. (set-array-element array 1 2 3 5)
    D. (modify-aref array 1 2 3 5)

    正确答案: B
    解析: 使用setf和aref的组合可以修改数组中特定位置的元素值。aref用于指定要修改的元素,setf用于设置新值。
    速记提示: “setf with aref” – 记住setf和aref的组合用于修改数组元素。

    知识点: 循环遍历哈希表的键值对
    题目: 在Common Lisp中,如何使用loop宏同时遍历哈希表hash的键和值?
    选项:
    A. (loop for (k . v) in hash do (format t “~A. ~A~%” k v))
    B. (loop for k being the hash-keys of hash using (hash-value v) do (format t “~A. ~A~%” k v))
    C. (loop for (k v) across hash do (format t “~A. ~A~%” k v))
    D. (loop for k v in hash do (format t “~A. ~A~%” k v))

    正确答案: B
    解析: 使用loop宏同时遍历哈希表的键和值,需要使用”being the hash-keys of”语法和”using (hash-value v)”子句。这允许同时访问每个键值对。
    速记提示: “hash-keys using hash-value” – 记住这个特殊的loop语法用于同时遍历哈希表的键和值。

    总结

    通过这20道题目,我们涵盖了Common Lisp中哈希表、关联列表、属性列表、数组和向量的主要概念和操作。以下是关键点总结:

    1. 哈希表:使用make-hash-table创建,gethash访问,setf和gethash组合添加或修改,remhash删除,clrhash清空。
    2. 关联列表(a-list):用cons创建键值对,list组合,assoc查找键,rassoc反向查找值。
    3. 属性列表(p-list):简单的交替键值列表,getf查找,remf删除。
    4. 数组:使用make-array创建,aref访问和修改元素。
    5. 向量:一维数组的特例,可用#(…)快速创建。
    6. 遍历:loop宏用于遍历各种数据结构,针对不同类型有特定语法。
    7. 符号属性:每个符号都有自己的属性列表,可通过symbol-plist访问。

    掌握这些概念和操作将帮助你更有效地使用Common Lisp进行编程。记住,选择合适的数据结构对于程序的效率和可读性至关重要。

    参考文献

    1. Common Lisp – The Tutorial Part 9.pdf
    2. https://github.com/rabbibotton/clog/blob/main/LEARN.md
    3. Huey Lewis and the News (歌词引用)
  • Common Lisp的艺术:从循环到格式化的美妙世界

    🌿 引言

    Roy Orbison的经典歌曲《Pretty Woman》唱道:“No one could look as good as you, mercy”。如果我们把这句话改编一下,大概也能形容Common Lisp的loop宏:“No one could look as complex as you, mercy”。确实,Common Lisp的loop宏既强大又复杂,初学者望而却步,老手也常常爱恨交加。但,正如那句老话所说:“熟能生巧”。今天,我们将一起走进这个充满魔力的世界,理解它的结构和美妙,并在最后聊聊format的神奇用法。


    ⚙️ Loop:循环的艺术

    你可能听说过,loop宏是Common Lisp中最具争议的特性之一。有人称它为“代码炼金术”,因为它可以将复杂的迭代逻辑简化为简洁的表达式;也有人认为它丑陋,与Lisp的优雅相违背。不过,无论你站在哪一边,loop的确是一个值得掌握的工具。

    让我们从loop的基本语法开始:

    (loop 
        [ 设置循环 ]
        do 
        [ 每次循环要做的事情 ]
    )

    虽然这个结构看起来很简单,但别被它骗了,它能做的事情远不止这些。比如,loop可以像for循环一样使用:

    (loop for N from 1 to 5 do (print N. )

    👣 步进与集合遍历

    loop中,你可以定义循环变量的步进方式,例如:

    • for N from 1 to 5 by 2:每次步进2
    • for N in '(1 2 3 4 5):遍历列表
    • for N across #(1 2 3 4 5):遍历向量

    这些功能让loop成为处理不同集合类型时极为灵活的工具。


    🔄 条件与重复

    有时候,我们希望循环在特定条件下执行,这就是“条件循环”的用武之地:

    • while:只要条件为真,循环继续
    • until:直到条件为真,循环停止
    (loop for y = 3 then (1- y) do (print y) until (= y 0))

    在这个例子中,循环从y = 3开始,每次减1,直到y等于0时停止。


    🎯 收集与计算

    loop不仅能执行操作,还能收集数据或进行计算。你可以使用以下关键词来收集或计算循环的结果:

    • collect:将每次循环的值收集到一个列表中
    • sum:计算循环变量的总和
    • maximizeminimize:查找最大或最小值
    • count:计算循环次数

    例如:

    (loop for x from 1 to 5 sum x)

    这个循环将返回1到5的总和。


    🎨 格式化:数据的艺术展示

    如果说loop是Common Lisp的“硬核”特性,那么format则是它的“柔情一面”。format函数不仅能将数据转换为字符串,还能生成各种格式的输出。其基本语法如下:

    (format stream control-string data1 data2…)

    其中,stream可以是t(表示标准输出),而control-string则是用于控制输出格式的指令。

    当我们想输出简单的字符串时,可以这样做:

    (format t "Hello, World!")

    这与(princ "Hello, World!")的效果相同。但format的强大之处在于它可以处理复杂的格式化需求。例如,~A指令可以将任何Lisp对象转换为字符串:

    (format t "Hello ~A, Lisp is cool!" "David")

    输出结果为:

    Hello David, Lisp is cool!

    💡 列表与递归格式化

    format的另一个强大功能是处理列表。通过使用~{}指令,我们可以对列表中的每个元素进行格式化:

    (format t “~{Element of List: ~A~%~}” ‘(1 2 3 4))

    输出结果为:

    Element of List: 1
    Element of List: 2
    Element of List: 3
    Element of List: 4

    这非常适合在处理数据结构时生成漂亮的输出。让我们逐步分析这段Common Lisp代码:

    背景介绍

    format是Common Lisp中的一个强大的输出格式化函数,能够根据给定的格式字符串输出文本。它可以将数据以多种方式格式化,并且支持复杂的格式控制。

    逐行解释

    1. (format t ...):
    • format函数的第一个参数t表示输出到标准输出(通常是控制台)。如果你想将输出保存到字符串中,可以使用nil或其他输出流。
    1. "~{Element of List: ~A~%~}":
    • 这是格式字符串,包含了格式控制符。
    • ~{ ... ~}: 这是一个循环格式控制符,表示对后面的列表进行迭代处理。
    • Element of List:: 在每次迭代中,这个字符串将与列表中的每个元素一起输出。
    • ~A: 这是一个格式控制符,表示以“人类可读”的格式输出其后面的参数(在这里是列表中的元素)。
    • ~%: 这个控制符表示输出一个换行符。每个元素之后都会换行。
    1. '(1 2 3 4):
    • 这是一个列表,包含数字1、2、3和4。单引号(')表示这是一个常量列表,而不是一个表达式。

    整体功能

    将这整段代码结合起来,它的作用是遍历列表'(1 2 3 4),对每个元素执行格式化输出,输出结果如下:

    Element of List: 1
    Element of List: 2
    Element of List: 3
    Element of List: 4

    难点和要点讲解

    • 循环格式控制符~{ ... ~}:
    • 这个控制符允许我们对列表的每个元素执行相同的格式化操作,非常适合在输出时需要重复模式的场景。
    • 输出控制符的组合使用:
    • ~A~%的结合使用使得每个元素后都能换行输出,这是非常常见的输出格式需求。
    • 列表的处理:
    • Common Lisp对列表的处理非常灵活,使用单引号定义常量列表让我们能够方便地直接使用和输出。


    📚 结论

    通过这篇文章,我们浅尝了Common Lisp中两个最具代表性的特性:loopformat。它们一个是功能强大的循环控制工具,另一个则是灵活的格式化输出利器。虽然它们最初看起来可能让人望而生畏,但一旦掌握,它们将成为你编程工具箱中不可或缺的利器。

    对于那些对loopformat感兴趣的读者,我强烈推荐进一步阅读相关文档和在线资源。正如Roy Orbison所唱的:“No one could look as good as you, mercy”,这些工具也是如此——一旦你理解了它们的美妙之处,你就会发现它们的无穷魅力。


    📖 参考文献

    1. Common Lisp Loop Tutorial
    2. Common Lisp Format Reference
    3. The CLHS pages on Format
    4. Jean-Philippe Paradis, Hexstream

    面向记忆的学习材料

    帮助用户快速学习并记住Common Lisp中Loop和Format的基本用法和概念。

    知识点: Loop的基本结构
    题目: 以下哪个是Loop宏的基本结构?
    选项:
    A. (loop [设置循环] collect [每次执行的操作])
    B. (loop [设置循环] do [每次执行的操作])
    C. (loop [设置循环] repeat [每次执行的操作])
    D. (loop [设置循环] for [每次执行的操作])

    正确答案: B
    解析: Loop宏的基本结构是(loop [设置循环] do [每次执行的操作])。其中,”do”关键字用于指定每次循环需要执行的操作。”do”也可以写成”doing”。
    速记提示: 记住”do”是执行的关键,就像”做”这个动作。

    知识点: Loop的For样式循环
    题目: 以下哪个不是Loop中For样式循环的正确用法?
    选项:
    A. for N from 1 to 5
    B. for N in ‘(1 2 3 4 5)
    C. for N across #(1 2 3 4 5)
    D. for N between 1 and 5

    正确答案: D
    解析: Loop中For样式循环的正确用法包括:from…to、in、across等。选项D中的”between…and”不是Loop中的标准语法。


    速记提示: 记住常用的”from to”、”in”和”across”,排除不常见的表达。

    知识点: Loop的While/Until样式循环
    题目: 以下哪个是Loop中While样式循环的正确用法?
    选项:
    A. for N = X then Z while (condition) do (body)
    B. for N = X then Z until (condition) do (body)
    C. while N = X then Z do (body)
    D. until N = X then Z do (body)

    正确答案: A
    解析: Loop中While样式循环的正确用法是:for N = X then Z while (condition) do (body)。这里,X是初始值,Z是每次循环的递增值,while后面跟随循环继续的条件。

    1727243445242


    速记提示: 记住”for…then…while”的结构,表示”从…然后…当…时”。

    知识点: Loop的Repeat用法
    题目: 如何使用Loop的repeat关键字重复执行5次操作?
    选项:
    A. (loop repeat 5 do (print “Hello”))
    B. (loop 5 times do (print “Hello”))
    C. (loop for i in 1 to 5 do (print “Hello”))
    D. (loop while i < 5 do (print “Hello”))

    正确答案: A
    解析: 使用Loop的repeat关键字重复执行操作的正确方式是:(loop repeat 5 do (print “Hello”))。这将打印”Hello”5次。
    速记提示: “repeat”直接跟数字,简单明了。

    知识点: Loop的条件语句
    题目: 以下哪个不是Loop中的条件语句?
    选项:
    A. if (condition) do (body) else do (body) end
    B. when (condition) do (body) end
    C. unless (condition) do (body) end
    D. do (body) until (condition) end

    正确答案: C
    解析: Loop中的条件语句包括if、when和do…until。”unless”不是Loop中的标准条件语句。
    速记提示: 记住常用的”if”、”when”和”until”,排除不常见的”unless”。

    知识点: Loop的真/假条件
    题目: 哪个Loop关键字用于检查循环中是否存在满足条件的情况?
    选项:
    A. always
    B. never
    C. thereis
    D. sometimes

    正确答案: C
    解析: “thereis”关键字用于检查循环中是否存在满足条件的情况。如果存在,则返回true。
    速记提示: “thereis”可以理解为”有没有”,询问是否存在。

    知识点: Loop的Initially/Finally
    题目: 如何在Loop循环开始前执行一些代码?
    选项:
    A. before (body)
    B. start (body)
    C. initially (body)
    D. begin (body)

    正确答案: C
    解析: 使用”initially”关键字可以在Loop循环开始前执行一些代码。例如:(loop initially (print “start”) for y from 1 to 5 do (print y))
    速记提示: “initially”意为”最初”,正好对应循环开始前。

    知识点: Loop的解构
    题目: 以下哪个是Loop中正确的解构绑定用法?
    选项:
    A. (loop for (a b) of ‘((1 2) (3 4) (5 6)) do (print (list a b)))
    B. (loop for (a b) from ‘((1 2) (3 4) (5 6)) do (print (list a b)))
    C. (loop for (a b) in ‘((1 2) (3 4) (5 6)) do (print (list a b)))
    D. (loop for (a b) = ‘((1 2) (3 4) (5 6)) do (print (list a b)))

    正确答案: C
    解析: Loop中正确的解构绑定用法是使用”in”关键字,如:(loop for (a b) in ‘((1 2) (3 4) (5 6)) do (print (list a b)))。这样可以将每个子列表的元素分别绑定到a和b。
    速记提示: 记住”in”是用于遍历列表的关键字。

    知识点: Loop的收集关键字
    题目: 以下哪个不是Loop中的数据收集关键字?
    选项:
    A. collect
    B. append
    C. nconc
    D. gather

    正确答案: D
    解析: Loop中的数据收集关键字包括collect、append、nconc、count、sum、maximize和minimize。”gather”不是标准的Loop收集关键字。
    速记提示: 记住常用的”collect”、”append”和”nconc”,排除不常见的”gather”。

    知识点: Loop的变量声明
    题目: 如何在Loop中声明一个新的循环变量?
    选项:
    A. declare new-loop-var
    B. let new-loop-var
    C. with new-loop-var
    D. var new-loop-var

    正确答案: C
    解析: 在Loop中使用”with”关键字可以声明一个新的循环变量。例如:(loop with x = 0 for y from 1 to 5 do (setf x (+ x y)))
    速记提示: “with”在英语中表示”带有”,这里表示循环带有一个新变量。

    知识点: Format函数的基本语法
    题目: Format函数的基本语法是什么?
    选项:
    A. (format stream control-string data1 data2…)
    B. (format control-string stream data1 data2…)
    C. (format data1 data2… control-string stream)
    D. (format data1 data2… stream control-string)

    正确答案: A
    解析: Format函数的基本语法是(format stream control-string data1 data2…)。其中stream可以是t(标准输出)、nil(返回字符串)或一个流对象。
    速记提示: 记住顺序:首先指定输出位置(stream),然后是控制字符串,最后是数据。

    知识点: Format的流参数
    题目: 在Format函数中,如果想要返回格式化后的字符串而不是直接输出,stream参数应该设置为什么?
    选项:
    A. t
    B. nil
    C. string
    D. return

    正确答案: B
    解析: 在Format函数中,如果将stream参数设置为nil,函数将返回格式化后的字符串,而不是直接输出到标准输出流。
    速记提示: nil表示”无”,这里表示不输出到任何流,而是返回字符串。

    知识点: Format的基本指令
    题目: 在Format函数中,哪个指令用于插入换行符?
    选项:
    A. ~N
    B. ~L
    C. ~%
    D. ~R

    正确答案: C
    解析: 在Format函数中,~%指令用于插入换行符。例如:(format t “Hello~%World”) 将输出两行文本。
    速记提示: %符号在很多编程语言中都用于表示特殊字符,这里用于换行。

    知识点: Format的~A指令
    题目: Format函数中的~A指令的作用是什么?
    选项:
    A. 将参数转换为ASCII码
    B. 将参数转换为数组
    C. 将任何Lisp类型转换为其打印表示
    D. 将参数转换为地址

    正确答案: C
    解析: Format函数中的~A指令用于将任何Lisp类型转换为其打印表示。它是一个通用的参数转换器。
    速记提示: A可以理解为”Any”,表示可以处理任何类型。

    知识点: Format的列表处理
    题目: 在Format函数中,如何处理列表中的每个元素?
    选项:
    A. 使用~L…~L指令
    B. 使用~E…~E指令
    C. 使用~{…~}指令
    D. 使用~[…~]指令

    正确答案: C
    解析: 在Format函数中,使用~{…~}指令可以处理列表中的每个元素。例如:(format t “~{Element: ~A~%~}” ‘(1 2 3 4))
    速记提示: 花括号{}在很多语言中用于表示代码块或集合,这里用于处理列表集合。

    知识点: Format的数值格式化
    题目: 在Format函数中,哪个指令用于以固定小数位数格式化浮点数?
    选项:
    A. ~D
    B. ~F
    C. ~E
    D. ~G

    正确答案: B
    解析: 在Format函数中,~F指令用于以固定小数位数格式化浮点数。例如:(format nil “~,2F” 3.14159) 将输出”3.14″。
    速记提示: F可以理解为”Fixed”,表示固定小数位数。

    知识点: Format的条件指令
    题目: Format函数中,哪个指令用于根据参数值选择不同的输出格式?
    选项:
    A. ~{…~}
    B. ~[…~]
    C. ~(…)
    D. ~<…~>

    正确答案: B
    解析: 在Format函数中,~[…~]指令用于根据参数值选择不同的输出格式。例如:(format nil “~[零~;一~;二~:;很多~]” 2) 将输出”二”。
    速记提示: 方括号[]常用于表示选择或索引,这里用于根据索引选择输出。

    知识点: Format的递归处理
    题目: 在Format函数中,如何递归处理嵌套的列表结构?
    选项:
    A. 使用~{~{…~}~}
    B. 使用~[~[…~]~]
    C. 使用~(~(…~)~)
    D. 使用~<~<…~>~>

    正确答案: A
    解析: 在Format函数中,可以使用嵌套的~{…~}指令来递归处理嵌套的列表结构。例如:(format nil “~{(~{~A~^ ~})~^ ~}” ‘((1 2 3) (4 5 6)))
    速记提示: 嵌套的花括号表示嵌套的列表处理。

    知识点: Format的大小写转换
    题目: 在Format函数中,哪个指令用于将输出转换为大写?
    选项:
    A. ~U
    B. ~C
    C. ~:@(
    D. ~S
    正确答题: C

    解析: 在Format函数中,~:@(指令用于将输出转换为大写。例如:(format nil “~:@(hello, world~)”) 将输出”HELLO, WORLD”。
    速记提示: @符号常用于表示特殊操作,这里用于大写转换。

    总结

    本学习材料涵盖了Common Lisp中Loop和Format的基本概念和用法。Loop宏是一个强大的迭代工具,提供了多种循环方式,包括For样式、While/Until样式、条件循环等。它还支持数据收集、变量声明和解构绑定等高级特性。Format函数是Lisp中通用的数据到字符串的转换工具,提供了丰富的指令来控制输出格式,包括基本的字符串插入、数值格式化、列表处理和条件输出等。掌握这些工具将大大提高你的Lisp编程效率和代码可读性。

    参考文献

    1. Common Lisp – The Tutorial Part 8.pdf
    2. http://www.lispworks.com/documentation/lw50/CLHS/Body/22_ck.htm
    3. https://www.hexstreamsoft.com/articles/common-lisp-format-reference/clhs-summary/
  • 💫 闭包、循环与字符串:Lisp世界的冒险之旅


    “生活有时像个圈,但在Lisp的宇宙中,圈圈转动时,奇妙的冒险才刚刚开始。”
    ——《Lisp 冒险指南》,第7章

    🌀 闭包:函数的记忆宫殿

    闭包是编程世界里的一个奇妙概念,在Lisp中,它像是一个让函数“记住”过去的魔法师。想象一下,你在做一个魔术箱,每次打开它,里面都能出现不同的东西,而这些东西总是和你之前放进去的有关。闭包就是这样一个“记忆箱”。

    🧠 词法作用域与闭包的魔法

    让我们从一个简单的例子开始:

    (defun my-adder (grow-by)
      (let ((sum 0))
        (lambda ()
          (incf sum grow-by))))

    在这个例子中,my-adder 是一个生成“加法器”的“工厂函数”。而lambda表达式则是那个藏着魔法的地方,它可以记住sum的状态,即使my-adder的执行上下文已经消失。

    当我们调用my-adder时,它会返回一个闭包,而这个闭包不仅仅是一个普通的函数,它还带着sum的记忆。于是,当我们多次调用生成的闭包时,sum会随着它的“历史”不断增长:

    (defvar *two-counter* (my-adder 2))
    (funcall *two-counter*) ; => 2
    (funcall *two-counter*) ; => 4

    每调用一次,sum都会累加2,闭包悄悄地记住了前一次的结果。是不是有点像你每次去冰箱拿吃的,冰箱总能记住你上次吃了什么?

    🔄 循环:Lisp的旋转木马

    Lisp中的循环简直就是编程界的旋转木马。你以为它只是原地打转?不不不!它每转一圈都能带你发现新风景。

    🎠 Loop:永不停息的循环

    首先介绍一下Lisp的loop,它是个拥有自己“语言”的循环工具。乍一看,它可能有点像个无尽的旋转木马:

    (let ((n 0))
      (loop
        (princ ".")
        (if (> n 10)
            (return n)
            (incf n))))

    上面的代码会在屏幕上不停地打印.,直到n大于10为止。每次loop转动,就好像你坐在木马上,随着音乐“咚咚咚”地前进。

    Dotimes:数数游戏

    如果你只是想数数,那dotimes是你的好伙伴:

    (dotimes (n 10)
      (princ "."))

    这段代码会打印10个点,每次循环n的值都会从0加到9。你可以把它想象成一个倒计时器,数到“10”时,游戏结束。

    🧩 Dolist:列表的漫游者

    有时候,你可能想要在一个列表中漫步,dolist就是为此而生的:

    (dolist (n '(1 2 3 4 5))
      (princ n))

    这段代码会依次输出列表中的每个元素,仿佛你在一个展览馆中,逐一欣赏墙上的每一幅画。

    🎮 Do:多任务处理的高手

    如果你是个多任务处理的高手,那么do循环一定能让你感到心满意足。它可以同时处理多个变量,像这样:

    (do ((x 1 (+ x 1))    ; 第一个循环变量
         (y 10 (- y 1)))   ; 第二个循环变量
        ((> x 10))         ; 终止条件
      (princ x)
      (princ " - ")
      (princ y)
      (terpri))

    这个循环在x增加的同时,y逐渐减少,直到x大于10时循环停止。就好像你在玩一个双人游戏,一个人向前跑,另一个人向后退。

    🔠 字符串:Lisp的字符魔法

    Lisp中的字符串操作也是一门很有趣的魔法。字符串不仅仅是字符的集合,它们还可以被像数组一样操作。

    🪄 常用的字符串咒语

    在Lisp里,这些字符串操作符就像魔法咒语一样:

    (length "Lisp")          ; 返回字符串的长度
    (string-upcase "Lisp")    ; 将字符串转换为大写 "LISP"
    (string-downcase "LISP")  ; 将字符串转换为小写 "lisp"
    (subseq "magical" 0 3)    ; 获取子字符串 "mag"
    (concatenate 'string "magic" "al") ; 字符串拼接 "magical"

    你可以用这些咒语轻松地操控字符串。比如,concatenate 可以将两个字符串合并为一个,就像用胶水粘合两块拼图。

    🎭 字符与字符串的转换

    在Lisp中,字符和字符串之间的转换也十分简单:

    (char "magic" 0)         ; 获取第一个字符 #\m
    (setf (char "magic" 0) #\M. ; 改变第一个字符为 #\M

    这就像你可以随意调整一段话中的某个字母,而不需要重新写整个句子。

    🤓 结语:Lisp的魅力无穷

    从闭包的记忆宫殿,到循环的旋转木马,再到字符串的字符魔法,Lisp无疑是一个充满奇妙工具和概念的编程语言。它不仅历史悠久而且功能强大,Lisp中那些看似简单的语法糖背后,蕴含着深厚的数学与计算机科学基础。

    所以,如果你还没试过Lisp,何不乘上这个旋转木马,体验一下编程世界中的这场奇幻冒险呢?


    📚 参考文献

    1. McCarthy, J. (1960). Recursive Functions of Symbolic Expressions and Their Computation by Machine. Communications of the ACM.
    2. Botton, R. (2023). Clog: Learn Lisp the Fun Way. GitHub Repository. https://github.com/rabbibotton/clog
  • 为什么选择 Lisp?🤔

    Lisp,一种源自1960年代的编程语言家族,以其独特的语法和功能式编程范式闻名。自问世以来,它一直是编程语言中的一个“怪咖”,但也是一颗璀璨的明珠。那么,为什么我们需要关心 Lisp?让我们来深入探讨一下它的魅力。

    语法简洁,规则至上 🧩

    Lisp 的语法可以用两个简单的规则来概括:

    1. 符号表达式(S-expression):Lisp 程序是由符号表达式组成的,符号表达式可以是数字、字符串、符号等字面量,或者是由这些字面量组成的列表。列表使用括号包裹,看起来像这样:(name arg1 arg2 ...)
    2. 函数调用与宏调用:在 Lisp 中,函数和宏都是通过列表形式调用的,列表的第一个元素是函数或宏的名字,后面是参数。

    这看起来似乎过于简单,但正是这种简化,使得 Lisp 拥有了强大的表达能力和灵活性。因为代码本质上就是数据,我们可以编写操作代码的代码,这也就是 Lisp 的系统的核心。

    (let ((x 1) (y 1))
      (if (> x 0)
          (+ x y)
          y))

    上面的代码展示了典型的一段 Lisp 表达式。我们定义了两个局部变量 xy,然后通过一个条件语句检查 x 是否大于 0。如果是,则返回 x + y,否则返回 y

    强大的宏系统 🛠️

    与大多数编程语言不同,Lisp 的宏不仅仅是“函数”。宏可以操作代码本身,接收未求值的代码作为输入,并生成新的代码。通过宏,程序员可以扩展语言的语法,创建适合特定需求的抽象。

    让我给你一个简单但生动的例子。假设我们要实现一个 and 函数,接受两个布尔值并返回 true 仅当两个参数都为 true 时。

    函数版本:🚫

    (defn my-and (a b)
      (if a b nil))

    这个函数看起来不错,但有个问题:它没有短路求值。也就是说,即使第一个参数为 false,第二个参数也会被求值。而在很多编程语言中,and 操作符会在第一个参数为 false 时直接返回 false,不会再去求值第二个参数。

    宏版本:✅

    (defmacro and (a b)
      `(if ,a ,b nil))

    在宏版本中,我们通过 defmacro 创建了一个宏。这个宏不会立即对参数求值,而是生成一个 if 语句。如果第一个参数为真,才会求值第二个参数。这样,我们就实现了短路求值。

    为什么 Lisp 的宏如此强大?💪

    宏的强大之处在于它们不仅能生成代码,还可以改变代码的行为。你可以将宏视为“代码生成器”。通过宏,我们可以轻松地定义新的控制结构、流程逻辑,甚至是全新的语法。换句话说,Lisp 使我们不仅可以使用语言,还可以扩展语言。

    一个更复杂的例子:动态创建代码 🧙‍♂️

    假设我们想要创建一个宏,它根据某些条件动态生成不同的代码。这在传统编程语言中可能需要复杂的条件判断和代码生成工具,但在 Lisp 中,宏让这一切变得简单自然。

    (defmacro when (condition &rest body)
      `(if ,condition
           (progn ,@body)))

    这个 when 宏类似于 if,但它只在条件为真时执行多行代码。progn 是 Lisp 中的一个特殊形式,表示顺序执行多条语句。通过宏,我们可以将这些操作封装起来,简化代码的编写。

    相比其他语言的优势 🌟

    现在你可能会问:“其他语言也有函数,为什么 Lisp 的宏如此特别?”我们来看一些常见的语言,比如 Python。当你写下 example(a, b, c) 时,Python 会先求值 abc,然后再调用 example 函数。但 Lisp 的宏不同,它不会立即求值,而是直接接收代码并返回修改后的代码。这种能力让 Lisp 编程语言能够以极高的灵活性处理复杂的编程需求。

    Python 的局限 🐍

    在 Python 中,虽然你可以编写函数,但要扩展语言的语法却十分困难。编写新的控制结构、在运行时动态生成代码,往往需要借助于元编程或复杂的解析工具。而在 Lisp 中,这一切都可以通过宏轻松实现。

    Lisp 的 introspection(自省能力) 🔍

    由于 Lisp 的代码本质上就是数据,Lisp 拥有非凡的自省能力。你可以在运行时检查代码结构、修改代码,甚至生成新的代码。这种自省能力在调试、元编程和性能优化中尤为有用。

    总结 🎯

    Lisp 以其简洁的语法和强大的宏系统,提供了其他语言难以企及的灵活性和表达能力。通过宏,程序员可以根据需求创建新的控制结构、流程逻辑,甚至是全新的语言特性。而这一切,都只需要遵循 Lisp 的两个核心规则。

    所以,为什么选择 Lisp?因为它不仅仅是一门编程语言,更是一个可以被程序员自由定制的工具箱。Lisp 让你不仅是程序的使用者,更是语言的创造者。它能让复杂的问题变得简单,让简单的代码变得优雅。


    🌟 携手宏世界:Lisp 中的宏与代码魔法

    在编程语言的世界里,Lisp 是一个特殊的存在。它不仅让你写程序,还可以让你写出可以写程序的程序!听起来很酷吧?在 Lisp 的宇宙中,宏(macros)就是这样一个魔法工具,它能让你不仅仅是编写代码,而是设计出新的控制结构和语言特性。今天,我们就来一探 Lisp 宏的奥秘,并探索其背后的思想。


    🧠 什么是宏?

    简单来说,宏是通过代码转换(transformation)实现的特殊操作符。宏不是在运行时执行的,而是在编译时就会被展开(expand)为更基础的 Lisp 表达式。换句话说,宏不仅仅是一个函数,它是一个“编译时的函数”,通过转换代码来生成新的代码。

    宏的定义

    在 Lisp 中,我们用 defmacro 来定义宏。与 defun 类似,但宏的作用是重写代码,而不是返回一个值。让我们来看一个简单的例子:

    (defmacro nil! (x)
      `(setf ,x nil))

    这个宏 nil! 会将它的参数设置为 nil。所以,当我们调用 (nil! a) 时,Lisp 实际上会将其展开为 (setf a nil),然后执行。

    > (nil! x)
    NIL
    > x
    NIL

    是不是很神奇?我们定义了一个新的操作符 nil!,但它的作用仅仅是把变量设置成 nil


    🛠️ 宏的秘密:代码与表达式的转换

    Lisp 的宏之所以强大,是因为它允许我们在编译时操作代码。这意味着我们可以在编译时对代码进行转换,从而生成更为高效或简洁的代码。在 Lisp 中,宏的展开过程称为“宏展开”(macro-expansion)。要查看宏展开的结果,可以使用 macroexpand-1 函数:

    > (macroexpand-1 '(nil! x))
    (SETF X NIL)
    T

    macroexpand-1 会将宏调用展开为其最终生成的代码。比如上面的例子中,(nil! x) 展开为了 (setf x nil)


    ⚡ 求值与效率问题

    在 Lisp 中,eval 函数可以将列表作为代码求值,但这种做法并不推荐。原因有两点:

    1. 效率低下eval 需要处理原始列表,编译和解释的效率都较低。
    2. 词法语境问题eval 无法引用局部的词法环境,特别是在 let 表达式中。

    因此,Lisp 提供了宏作为更优雅的解决方案。宏会在编译时展开成标准的 Lisp 表达式,避免了运行时的开销。


    🔧 反引号与逗号:宏定义的好帮手

    在宏定义中,有两个非常有用的符号:反引号`)和 逗号,)。反引号允许你创建一个模板,而逗号则允许你在模板中插入求值结果。

    看看这个例子:

    (defmacro nil! (x)
      `(setf ,x nil))

    这里的反引号表示整个表达式是一个模板,而逗号用来插入 x 的值。如果没有反引号和逗号,我们就得手动构建列表,增加了代码的复杂性。


    🌀 宏的高级用法:快速排序

    为了展示宏的强大,我们来看一个复杂的例子:使用宏来实现快速排序(QuickSort)。以下是使用宏的快速排序代码:

    (defun quicksort (vec l r)
      (let ((i l) (j r)
            (p (svref vec (round (+ l r) 2))))
        (while (<= i j)
          (while (< (svref vec i) p) (incf i))
          (while (> (svref vec j) p) (decf j))
          (when (<= i j)
            (rotatef (svref vec i) (svref vec j))
            (incf i)
            (decf j)))
        (if (>= (- j l) 1) (quicksort vec l j))
        (if (>= (- r i) 1) (quicksort vec i r))
        vec))

    在这个例子中,我们使用了 while 宏来简化循环逻辑。while 宏的定义如下:

    (defmacro while (test &rest body)
      `(do () ((not ,test)) ,@body))

    通过使用宏,我们使得代码更为简洁,并避免了重复的代码块。快速排序算法通过递归地将数组分为两部分,并对每部分进行排序,最后返回排序后的数组。


    📉 宏的设计原则

    设计宏时,需要注意以下两个问题:

    1. 变量捕捉:宏展开时可能会引入与现有变量同名的局部变量,导致意想不到的行为。解决这个问题的常用方法是使用 gensym 函数生成一个唯一的符号,避免变量名冲突。
    2. 多重求值:宏的参数可能会被多次求值,如果参数带有副作用,这可能会导致错误的结果。为避免这个问题,可以在宏展开时将参数存储到局部变量中。

    让我们来看如何解决这两个问题:

    (defmacro ntimes (n &rest body)
      (let ((g (gensym)) (h (gensym)))
        `(let ((,h ,n))
           (do ((,g 0 (+ ,g 1)))
               ((>= ,g ,h))
             ,@body))))

    在这个 ntimes 宏中,我们使用了 gensym 生成唯一的符号,避免了变量捕捉的问题。同时,我们也确保参数 n 只会被求值一次,避免多重求值的副作用。


    🌐 宏的未来:Lisp 的无限可能

    宏是 Lisp 强大灵活性的核心。通过宏,程序员可以在编译时改写代码,创建新的控制结构,甚至设计出自己的语言特性。宏不仅仅是为了减少代码量,更重要的是它们使得程序更具可读性和可维护性。

    正如 Lisp 的哲学所说:“Lisp 是一种可被程序员塑造的语言。”通过宏,程序员可以将 Lisp 变成任何他们想要的样子。


    📚 参考文献

    1. ANSI Common Lisp 中文版 — 第十章:宏
    2. Graham, P. (1995). On Lisp: Advanced Techniques for Common Lisp.
    3. Norvig, P. (1992). Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp.

    在 Common Lisp 中,逗号 , 通常出现在宏的上下文中,特别是在使用 反引号(backquote,或称“准引用”)时。理解这个符号的关键在于它和反引号的关系。

    反引号和逗号的用法:

    1. 反引号(` 或称 backquote):**
    • 反引号允许构造一个模板,在模板中某些部分是常量,而其他部分可以根据上下文进行求值。
    1. 逗号,):**
    • 当在反引号模板中使用时,逗号表示“在这里插入求值后的表达式的结果”。换句话说,逗号会告诉 Lisp 求值某个特定的表达式并将其插入到模板中,而不是把表达式作为字面量。

    例子

    `(1 2 ,x 4)

    在这个例子里,124 是常量,直接出现在结果列表中,而 ,x 表示“将变量 x 的当前值插入到这个位置”。如果 x 的值是 3,那么这个表达式会被展开为:

    (1 2 3 4)

    在上面的例子中:

    (setf ,x nil)

    此表达式中的逗号表明它是出现在一个反引号表达式内部的。假设你在一个宏中使用了反引号,x 是一个变量或参数,在宏运行时需要插入到 setf 表达式中。

    举个宏的例子:

    (defmacro example-macro (x)
      `(setf ,x nil))

    在这个宏中,x 是宏的参数。反引号表示接下来构造一个模板,而 ,x 告诉 Lisp 在这段模板中插入 x 的值。假设你调用这个宏时传入的是变量 a

    (example-macro a)

    这个表达式会被展开为:

    (setf a nil)

    即,它将 a 的值设置为 nil

    总结

    • 反引号:用于构造模板,其中一些部分是常量,其他部分可以是求值结果。
    • 逗号:在反引号上下文中,表示“求值”后插入结果。
    • setf ,x nil 中,逗号意味着 x 是一个需要在宏展开时被求值替换的变量,而不是字面量。

    在 Common Lisp 中,gensym 是一个生成“符号”的函数,常用于宏编程中。它的主要功能是创建一个全局唯一的、不与现有符号冲突的新符号,通常用于避免变量名冲突。

    为什么需要 gensym

    在编写宏时,宏展开可能会与用户定义的变量名产生冲突。例如,如果你在宏中使用了一个局部变量,但用户在调用宏时也使用了同名的变量,那么可能会导致意料之外的行为。为了解决这种问题,gensym 可以生成唯一的符号,从而避免这种冲突。

    gensym 的基本用法

    gensym 可以简单地调用而不带任何参数,或带一个可选字符串前缀。它返回一个唯一的符号。

    1. 不带参数的使用:

    (gensym)

    这将生成一个唯一的符号,通常像 G1234 这样,其中数字部分是一个自增的计数器。

    2. 带参数的使用:

    (gensym "temp-")

    这将生成一个符号,前缀为 temp-,例如 temp-1234

    例子

    宏编程的一个典型例子是使用 gensym 来避免变量捕获。假设我们想编写一个简单的 with-temp-variable 宏,它会创建一个临时变量并执行一些代码:

    (defmacro with-temp-variable (var &body body)
      `(let ((,var (gensym)))
         ,@body))

    这个宏在展开时会创建一个临时的 var,并执行 body。不过,如果用户在调用宏时使用了一个与宏内部变量同名的变量,就可能导致冲突。我们可以通过 gensym 来生成一个唯一的符号,避免这种冲突。

    使用 gensym 改进的宏:

    (defmacro with-temp-variable (&body body)
      (let ((temp-var (gensym "temp-var-")))
        `(let ((,temp-var nil))
           ,@body)))

    在这个宏中,temp-var 是通过 gensym 生成的唯一符号,所以无论用户传入什么样的变量名,都不会与这个临时变量冲突。

    在宏展开中的应用

    为了更好地理解 gensym 在宏中的实际用法,下面是一个经典的例子:编写一个 once-only 宏,它确保在宏展开时某个表达式只被求值一次。

    (defmacro once-only (var &body body)
      (let ((temp-var (gensym)))
        `(let ((,temp-var ,var))
           ,@body)))

    这个宏的作用是:对传入的 var 只求值一次,然后在宏体 body 中多次使用这个值。使用 gensym 生成的临时变量,避免了与用户传入的变量名冲突。

    使用示例:

    (let ((x 10))
      (once-only (+ x 1)
        (list temp-var temp-var)))

    在这个例子中,(+ x 1) 只会被求值一次,然后结果会存储在 temp-var 中。即使用户传入的表达式可能包含副作用,宏也保证了它只会被执行一次。

    总结

    • gensym 生成一个唯一的符号,常用于宏编程中,防止变量名冲突。
    • 不带参数时gensym 生成的符号通常形如 G1234
    • 带参数时,可以提供一个前缀,例如 temp-,生成的符号形如 temp-1234
    • 在宏展开时,gensym 是确保临时变量唯一性的重要工具,尤其是在避免捕获用户变量名时很有用。

    通过 gensym,我们可以确保宏中生成的符号是全局唯一的,从而避免了潜在的变量名冲突问题。


    在 Common Lisp 中,符号 @ 出现在宏定义的上下文中,特别是在 反引号(backquote 或 “准引用”)表达式中。它与逗号 , 配合使用,表示将一个列表的所有元素拼接到当前的列表中。它的作用是“解包”一个列表,并将其所有元素插入到目标列表中。

    ,@ 的作用

    • 逗号 ,:表示在反引号表达式内求值,替换为求值后的结果。
    • 逗号加上 @ (,@):表示将求值结果插入到列表中,并“解包”列表中的每个元素(即将列表的元素逐个插入),而不是将整个列表作为一个元素插入。

    例子

    假设我们有一个列表 body,其值是 (print 1) (print 2)

    (defmacro example (body)
      `(list ,@body))

    当你传入 (print 1) (print 2) 作为 body 时,@ 的作用是将整个 body 列表解包,将其内容直接拼接到 list 调用中。

    宏调用:

    (example ((print 1) (print 2)))

    宏展开后的结果:

    (list (print 1) (print 2))

    在这个例子中,body 是一个列表,,@body 表示将 body 列表的元素逐个插入到结果的 list 中,而不是将整个 body 列表作为一个元素插入。

    对比:用 ,,@ 的区别

    假设我们在宏中只用 ,body 而不是 ,@body

    (defmacro example (body)
      `(list ',body))

    在这种情况下,body 则作为一个整体被插入,而不会被解包成单独的元素。这里的 ‘ 表示不要求值body

    宏调用:

    (example ((print 1) (print 2)))

    宏展开后的结果:

    (list '((print 1) (print 2)))

    这里 body 作为一个整体被插入结果列表,变成了嵌套列表,而不是将 body 的元素直接插入。

    上面宏中的 ,@body

    回到上面的宏定义:

    (defmacro once-only (var &body body)
      (let ((temp-var (gensym)))
        `(let ((,temp-var ,var))
           ,@(mapcar (lambda (form)        ;; 遍历 body 中的每个表达式
                       (subst temp-var 'temp-var form))  ;; 把 'temp-var 替换为生成的 temp-var
                     body))))
    • &body body&body 是 Common Lisp 中的一种用法,类似于 &rest,用于接收不定数量的表达式,并将它们收集成一个列表。这里 body 是一个列表,包含宏调用时传入的所有表达式。
    • 反引号表达式 `:反引号表示我们正在构造一段代码模板,其中一些部分是常量,一些部分需要求值后插入。
    • ,@bodybody 是一个列表,,@body 表示将 body 列表的所有元素解包,并插入到展开后的代码中。

    示例展开

    假设你调用这个宏如下:

    (once-only (+ 1 2)
      (print temp-var)
      (print (* temp-var 2)))

    在这个调用中:

    • var(+ 1 2),表示我们传递了一个表达式。
    • body((print temp-var) (print (* temp-var 2))),这将被传递给宏。

    宏的展开过程如下:

    1. temp-var 被绑定为一个由 gensym 生成的唯一符号,例如 G1234
    2. 生成的代码会是:
    (let ((G1234 (+ 1 2)))
      (print G1234)
      (print (* G1234 2)))

    这里,body 中的每个表达式都被解包并插入到 let 表达式的主体中,正是通过 ,@body 实现的。如果没有 @,整个 body 列表会作为一个整体插入,导致语法错误。

    总结

    • ,@ 表示将一个列表的元素解包,并插入到当前上下文中。
    • 在宏中使用 ,@ 可以将一个列表的内容逐个插入,而不是将列表本身插入。
    • 在你的 once-only 宏里,,@body 使得 body 的每个表达式都被插入到生成的 let 表达式中,保证宏调用的主体能够正确展开。

    这个技巧在宏编程中非常常见,特别是在处理不定数量的表达式时,可以确保宏展开后的代码结构清晰正确。


    在 Common Lisp 中,# 符号的使用有多种形式,具体含义取决于上下文。在你提到的 #:G120 这样的符号中,#: 是一种特殊的符号表示法,称为 未绑定符号uninterned symbol)。让我们详细解释一下这个符号的用途以及 # 在不同场景下的含义。

    1. #:G120 中的 #: 是什么?

    • #: 前缀表示 未绑定符号uninterned symbol)。
    • 这些符号是 唯一的,并且不会在任何包(namespace)中注册。
    • 每次使用 #:symbol-name 这种表示法时,都会创建一个新的、唯一的符号。

    例子:

    (eq '#:foo '#:foo) ;; => NIL

    在这里,#:foo 是两个不同的未绑定符号,即使它们的名称相同,它们也是不同的对象。因此,eq 比较它们时返回 NIL,表示它们是不同的符号。

    与普通符号的区别:

    普通的符号(interned symbols)会被存储在某个包中(比如 COMMON-LISP-USER::FOO),并且具有全局唯一性。如果你在不同的地方使用相同名称的符号,它们会是同一个符号。

    (eq 'foo 'foo) ;; => T

    在这个例子中,foo 是一个包内的符号(interned symbol),因此它们是相同的符号,eq 返回 T

    2. 为什么要使用 #:

    在宏编程中,尤其是在使用 gensym 生成唯一符号时,通常会生成未绑定符号。使用 #: 可以避免命名冲突,因为这些符号不会和其他定义的符号冲突。

    例子:gensym

    gensym 函数通常生成未绑定符号。你可以认为 gensym 就是自动生成了类似 #:G120 这样的符号。

    (gensym) ;; => #:G120

    这种生成的符号不会和任何现有的符号冲突,因此在宏展开时非常有用,避免了与用户代码中的符号重名问题。

    3. # 在其他上下文中的含义

    除了 #: 表示未绑定符号外,# 在 Common Lisp 中还有许多其他用法。以下是一些常见的例子:

    • #' 表示 函数引用,用于引用函数对象。 例子:
      (mapcar #'print '(1 2 3)) ;; #'print 等价于 (function print)
    • #\ 表示 字符。用于表示单个字符的字面量。 例子:
      #\A ;; 表示字符 A
      #\Newline ;; 表示换行符
    • #( 表示 向量。表示一个向量的字面量。 例子:
      #(1 2 3) ;; 一个包含 1、2、3 的向量
    • #' 用于函数引用(与 function 等价)。 例子:
      (mapcar #'print '(1 2 3)) ;; 等价于 (mapcar (function print) '(1 2 3))
    • #. 表示 在读取时求值。会在读取时对表达式求值,并返回结果。 例子:
      #.(+ 1 2) ;; 读取时返回 3
    • #| ... |# 表示 注释块。用于注释掉一段代码。 例子:
      #| 这是一段注释 |#

    4. 回到上面的例子

    在你的宏定义中,使用了 gensym 来生成唯一符号。gensym 返回的符号类似于 #:G120,这是一个未绑定符号。宏展开后,#:G120 确保不会与用户代码中的任何符号冲突。

    宏定义:

    (defmacro once-only (var &body body)
      (let ((temp-var (gensym)))
        `(let ((,temp-var ,var))
           ,@(mapcar (lambda (form)
                       (subst temp-var 'temp-var form))
                     body))))

    当你调用宏时:

    (once-only (+ 1 2)
      (print temp-var)
      (print (* temp-var 2)))

    这个宏展开为:

    (let ((#:G120 (+ 1 2)))
      (print #:G120)
      (print (* #:G120 2)))

    这里,#:G120 是一个由 gensym 生成的未绑定符号,确保它不会和任何现有符号冲突。

    总结

    • #::用于表示 未绑定符号(uninterned symbol)。这些符号不会与任何包中的符号冲突,通常用于宏编程中生成唯一的符号。
    • gensym:是生成未绑定符号的常用方式,避免在宏展开过程中与用户定义的符号产生冲突。
    • # 在 Common Lisp 中有许多其他特殊用途,包括字符、向量、函数引用等。

  • 人生梦想 - 关注前沿的计算机技术 acejoy.com