标签: Lisp

  • 🌐 探索 Common Lisp 文档库的奇妙世界

    在编程的浩瀚宇宙中,Common Lisp 就像一颗闪亮的明星,吸引了无数编程爱好者的目光。而在这颗明星的周围,有一个令人兴奋的地方,那就是 Common Lisp Document Repository (CDR)。今天,我们就来一探这个文档库的奥秘,看看它是如何为编程者提供支持的。

    📚 CDR 的使命与特色

    CDR 的主要使命就像是为 Common Lisp 提供一个丰富的知识宝库。想象一下,一个令人眼花缭乱的图书馆,里面摆满了关于 Common Lisp 的书籍、文档和示例代码。这个文档库不仅仅是一个简单的存储空间,它还致力于为用户提供最全面、最有用的资源。

    在 CDR 中,你可以找到各种主题的文档,从基础的语言特性到高级的编程技巧,几乎应有尽有。它就像一位和蔼可亲的老教授,总是愿意分享他无尽的知识。

    🖥️ 如何访问 CDR

    当然,要想充分利用 CDR,你需要一个能够支持框架功能的网页客户端。正如一位不懂使用工具的画家无法创作出美丽的画作一样,使用不支持框架的浏览器就无法体验 CDR 的全部魅力。因此,确保你的浏览器能够支持框架功能,这样才能愉快地浏览这些宝贵的文档。

    🔍 深入文档的海洋

    在 CDR 中,文档的种类繁多,覆盖的主题范围极广。你可以找到关于 Common Lisp 标准的详细说明、库的使用指南,以及各类实用的编程示例。这些文档就像是编程者的航海图,帮助他们在编程的海洋中找到正确的航向。

    例如,如果你对宏(Macros)感兴趣,CDR 中有丰富的示例和解释,帮助你理解这一强大的功能。宏就像是编程中的魔法,可以让你以全新的方式思考代码的构造。

    📊 可视化文档库的结构

    为了更好地理解 CDR 的结构,我们可以使用图表来可视化它的内容。下面是一个简单的图表,展示了 CDR 中文档的分类。

    graph TD; A[Common Lisp Document Repository] –> B[基础文档]; A –> C[高级主题]; A –> D[库与工具]; B –> E[语言特性]; B –> F[编程示例]; C –> G[性能优化]; C –> H[并发编程]; D –> I[常用库]; D –> J[开发工具];

    这个图表展示了 CDR 的主要组成部分,让我们一目了然。

    🤖 CDR 的未来发展

    随着技术的不断进步,CDR 也在不断发展。未来,CDR 可能会引入更多互动性和动态内容,使得用户可以更加轻松地获取信息。同时,社区的参与也将是 CDR 发展的关键,更多的用户贡献自己的知识和经验,将使这个文档库变得更加丰富。

    总之,CDR 就像一片丰饶的土地,等待着编程者去耕耘和探索。无论你是刚入门的新手,还是经验丰富的老手,这里都能提供你所需的资料与灵感。

    📖 参考文献

    1. Common Lisp HyperSpec.
    2. Practical Common Lisp by Peter Seibel.
    3. ANSI Common Lisp.
    4. Common Lisp: A Gentle Introduction to Symbolic Computation by David S. Touretzky.
    5. Common Lisp Cookbook.

    在这个充满机遇的时代,别忘了访问 CDR,汲取知识的养分,让你的编程之旅更加精彩!

    CDR – Common Lisp Document Repository (common-lisp.dev)

  • 🐢 深入浅出Common Lisp函数的奥秘

    在编程的海洋中,Common Lisp就像一只悠然自得的海龟,缓慢却坚定地游向知识的彼岸。在这篇文章中,我们将一起探索Common Lisp函数的定义、参数以及高阶函数等内容,确保每位读者都能在这片代码的海洋中畅游自如。

    ✍️ 定义新函数

    在Common Lisp中,定义一个新函数就像给你的程序添置一个新工具,它将帮助你完成特定的任务。使用defun宏来定义函数,其基本结构如同一个简洁的说明书:

    (defun name
      "一些说明文字"
      body-form*)

    举个例子,我们来定义一个简单的“Hello, World”函数:

    (defun hello-world ()
      "打印Hello, world"
      (format t "Hello, world"))

    在这个例子中,hello-world是函数名,()表示这个函数不需要参数,而(format t "Hello, world")则是函数体,它的使命就是向标准输出打印“Hello, world”。

    📦 函数形参列表

    形参列表就像是函数的邀请函,只有接受了这些邀请,函数才能顺利运行。在定义函数时,形参列表用于声明一些变量来接收传递给函数的实际参数。普通的形参列表要求每一个必要参数都必须提供实参,否则函数会婉拒执行。

    可选形参

    有时候,实参数可能少于形参数,这时可选形参就派上用场了。通过在必要形参后使用&optional,未赋值的形参可以用默认值填充。比如:

    (defun foo (a b &optional c d)
      (list a b c d))

    这个函数可以这样调用:

    (foo 1 2)    ; -> (1 2 NIL)
    (foo 1 2 3)  ; -> (1 2 3 NIL)
    (foo 1 2 3 4); -> (1 2 3 4)

    如果想为可选形参提供默认值,只需这样写:

    (defun foo (a b &optional (c 10))
      (list a b c))

    这样调用(foo 1 2)时,c会默认取值10。

    🌊 剩余形参

    当你需要传递多个参数时,剩余形参就能派上用场。通过在必要和可选形参后使用&rest,所有后续的实参将被收集到一个列表中。比如:

    (defun + (&rest numbers)
      ...)

    这意味着你可以传递任意数量的参数,函数会将它们整理成一个列表。

    🔑 关键字形参

    如果你只想为某些参数提供值,关键字形参将是一个完美的解决方案。在必要和可选形参之后,使用&key来定义关键字形参,比如:

    (defun foo (&key a b c)
      (list a b c))

    这样调用(foo :a 1 :b 2)时,它会返回(1 2 NIL),方便快捷。

    🌀 混合不同的形参类型

    在一个函数中,你可以灵活地混合不同类型的形参。常见的组合是必要形参和可选形参,或者是&optional&rest的组合。顺序上,必要形参应该放在前面,后续可以添加可选、剩余和关键字形参。使用关键字形参可以让你的代码更加易于维护和扩展。

    🔄 函数返回值

    在Common Lisp中,函数的返回值默认是最后一个表达式的值,也可以使用return-from语句在任何位置返回值。例如,下面的函数寻找一个数对,要求每个数都小于10,并且它们的乘积大于参数n

    (defun foo (n)
      (dotimes (i 10)
        (dotimes (j 10)
          (when (> (* i j) n)
            (return-from foo (list i j))))))

    🎭 作为数据的函数——高阶函数

    在Common Lisp中,函数不仅可以执行操作,还可以作为数据存储在变量中,甚至传递给其他函数。使用defun定义的函数实际上创建了一个新的函数对象并赋予其一个名字。你也可以使用lambda表达式创建匿名函数。例如:

    (funcall #'(lambda (x y) (+ x y)) 2 3)  ; 返回5

    🕵️‍♂️ 匿名函数

    使用lambda创建匿名函数的语法如下:

    (lambda (parameters) body)

    例如,创建一个匿名函数来增加两个数并调用它:

    (funcall #'(lambda (x y) (+ x y)) 2 3)  ; 返回5

    通过这些示例,我们可以看到Common Lisp的函数机制是多么强大与灵活。无论是定义函数、处理形参,还是使用高阶函数,Lisp都能让你如鱼得水,尽情地在编程的海洋里遨游。

    📚 参考文献

    • 编程之禅. (2021). Common Lisp函数. Retrieved from 编程之禅
  • Common Lisp:语法的迷宫与乐趣 🎭

    在编程的世界中,Common Lisp就像一位优雅的舞者,虽然动作复杂却充满魅力。今天,我们将一起走进这个迷宫般的语法世界,探索那些看似繁琐却又极具魅力的细节。准备好了吗?让我们开始这场冒险吧!

    📝 注释:编程的隐秘语言

    编程中的注释就像是旅途中那些小小的指示牌,它们指引着我们前行的方向。在Common Lisp中,注释的方式多种多样:

    • ;;;;:文件头注释,适合于介绍整个文件的背景。
    • ;;;:用于描述一大段代码的作用,像是为代码添加了一个精致的封面。
    • ;;:几行代码的功能性描述,注释与被注释的代码保持相同缩进,清晰明了。
    • ;:单行注释,简单直接,像是随手写下的便签。

    通过这些注释,程序员们能够在代码的海洋中保持清醒,不至于迷失方向。

    📜 S-表达式:编程的心脏

    S-表达式是Common Lisp的核心,它就像是一种神奇的魔法符咒。S-表达式的基本元素是列表(list)和原子(atom),其中列表由括号围绕,而原子则是不可分割的元素。比如:

    (1 2 3)
    ("stf" 1 2 3)
    (foo 1 2 3)
    (foo 1 2 3 (getMax 4 5))

    这些都是有效的S-表达式。可以想象,它们就像是不同口味的冰淇淋,组合后产生了无穷的可能性。

    🔢 常见原子类型:数字与字符串的舞蹈

    在Common Lisp中,常见的原子类型包括数字、字符串和名字。让我们来看看这些类型是如何配合演出的。

    数字的魅力

    数字的表示形式多种多样,既可以是简单的整数,也可以是复杂的浮点数。例如:

    123      ; 整数
    +122     ; 正数
    -122     ; 负数
    12.1     ; 默认精度浮点数
    121e-1   ; 科学计数法表示
    -5/4     ; 比值

    这些数字就像是一群舞者,各自展现着独特的风采。

    字符串的旋律

    字符串则由双引号包围的可见字符组成,反斜杠(\)作为转义字符,帮助我们在字符串中插入特殊字符。例如:

    “foo”      ; 表示由f,o,o组成的字符串
    “fo\o”     ; 同一个字符串

    字符串的魅力在于它们可以灵活地组合,形成动人的旋律。

    名字的身份

    在Common Lisp中,函数名和变量名是最常见的名字。几乎任何字符都可以出现在名字里,但有十个特殊字符需要注意:

    • 开括号 (
    • 闭括号 )
    • 双引号
    • 单引号
    • 反引号 `
    • 逗号 ,
    • 冒号 :
    • 分号 ;
    • 反斜杠 \
    • 竖线 |

    如果真要使用这些特殊字符,得加上转义字符哦。例如,on\off就可以作为一个名字。

    🚀 Lisp形式的S-表达式

    在Common Lisp中,函数调用和特殊操作符都是以S-表达式的形式出现的。让我们来看几个例子:

    函数调用

    函数调用的基本形式是:

    (function-name argument*)

    例如:

    (+ 1 2)            ; +号为函数名
    (/ 2 (+ 1 1))    ; 最外层的/是函数名,内部嵌套的+也为函数名

    这样的结构就像是一个层层叠叠的蛋糕,每一层都充满了惊喜。

    特殊操作符

    并非所有操作都能定义成函数,特殊操作符便是其中的一类。例如条件判断语句:

    (if (x) (format t “yes”) (format t “no”))

    这段代码就像是一个简单的问答游戏,返回“yes”或“no”,让我们感受到编程的乐趣。

    📦 宏:编程的魔法师

    在Common Lisp中,宏是一种特殊的功能,它以S-表达式为参数,返回一个Lisp形式。宏的求值过程包括两个阶段:首先将元素传递到宏函数,然后根据正常的求值规则进行处理。

    当Lisp代码被编译时,源文件中的所有宏形式将被递归展开,最终编译成一个FASL文件。这就像是一个魔法师,将复杂的咒语变为简单的指令。

    ⚖️ 真假与等价

    在Lisp中,符号NIL表示唯一的假值,而其他所有值都是真值T. NIL的特别之处在于,它既是原子又是列表,既可以作为假值使用,也可以作为空列表(())。

    等价判断

    Lisp提供了多种等价判断方式:

    • EQ:是最严格的等价判断,只有对象相同时返回T.
    • EQL:更宽松,当两个对象表示相同的字符或数字时,即使不是同一对象,也返回T.
    • EQUAL:在递归上具有相同结构和内容的列表被视为等价。
    • EQUALP:忽略大小写的字符串判断,只要表示相同数学意义的值,它们就是等价的。

    例如:

    (EQUALP “helloworld” “HelloWorld”) ; 返回T
    (EQUALP 1 1.0) ; 返回T

    这样的等价判断让我们在编程时更加灵活,仿佛拥有了多种视角。

    🎹 Emacs小技巧

    在使用Emacs进行Common Lisp编程时,有一些技巧可以帮助我们提高效率:

    • 选中代码后,使用 C-M-q 重新缩进整个表达式。
    • 在函数体的任何位置,通过 C-c M-q 来缩进整个函数体。

    这些小技巧就像是编程中的调味品,让我们的代码更加优雅。

    结语

    通过这次探索,我们不仅了解了Common Lisp的语法规则,还领略了其中的魅力与乐趣。编程不再只是冷冰冰的代码,而是充满了创意与表达的艺术。在这个迷人的世界中,愿你能找到属于自己的舞步,与Common Lisp共舞!


    参考文献

    • Common Lisp 语法规则 | 编程之禅. 访问链接: 编程之禅
  • 用 Lisp 实现简单数据库的优雅之旅 🛤️

    在编程的世界里,语言的选择如同选择一位舞伴,有的轻盈优雅,有的沉稳可靠,而Lisp无疑是其中的优雅舞者之一。尽管诞生于1958年,Lisp依然在现代编程中大放异彩,正如Paul Graham所言:“Lisp是数学,数学永远不会过时。”今天,我们将通过一个简单的例子,来探讨如何用Lisp实现一个基本的数据库,专门用来存储MP3歌曲的信息。

    CD 和记录 🎵

    首先,我们需要定义我们的数据结构。我们的数据库将包含多条CD记录,每条记录包含以下四个信息:

    • CD标题
    • 艺术家信息
    • 评价信息(满分10分)
    • 是否被烧录(布尔值)

    1. 数据结构的定义

    在Lisp中,我们可以使用列表(list)和属性表(property list,简称plist)作为数据结构。列表类似于Python中的列表,而属性表则更像Python的字典。我们可以用以下代码定义一个CD记录:

    (defun make-cd (title artist rating ripped)
      (list :title title :artist artist :rating rating :ripped ripped))

    使用示例:

    (make-cd "Roses" "Kathy Mattea" 7 t)

    这将返回一个结构化的CD记录:

    (:TITLE "Roses" :ARTIST "Kathy Mattea" :RATING 7 :RIPPED T)

    2. 录入 CD 记录 📜

    接下来,我们需要一个地方来存储这些CD记录。我们可以定义一个全局变量*db*(遵循Lisp的命名约定),并利用PUSH宏来添加新的记录:

    (defvar *db* nil)
    
    (defun add-record (cd)
      (push cd *db*))

    现在,我们可以将make-cdadd-record结合起来,方便地将新的CD记录添加到数据库中。

    3. 数据库的格式化输出 🎉

    为了查看数据库中的内容,我们需要一个更友好的输出格式。我们可以使用dolist宏来遍历数据库,并用format函数来格式化输出:

    (defun dump-db ()
      (dolist (cd *db*)
        (format t "~{~a:~10t~a~%~}" cd)))

    调用(dump-db)后,我们将看到如下格式的输出:

    TITLE:    Pork Face
    ARTIST:   Laddy
    RATING:   9
    RIPPED:   T
    TITLE:    Roses
    ARTIST:   Kathy Mattea
    RATING:   7
    RIPPED:   T

    改进用户交互 💬

    使用add-record来添加CD记录显得有些繁琐,因此我们可以编写一个函数来提示用户输入CD信息。以下是一个简单的用户输入函数示例:

    (defun prompt-read (prompt)
      (format *query-io* "~a: " prompt)
      (force-output *query-io*)
      (read-line *query-io*))

    结合prompt-readmake-cd,我们可以创建一个更友好的用户接口:

    (defun prompt-for-cd ()
      (make-cd
       (prompt-read "Title")
       (prompt-read "Artist")
       (or (parse-integer (prompt-read "Rating") :junk-allowed t) 0)
       (y-or-n-p "Ripped [y/n]: ")))

    保存和加载数据库 💾

    为了防止数据丢失,我们可以将数据库保存到文件中,并能够在下次加载时读取。以下是保存和加载的代码示例:

    (defun save-db (filename)
      (with-open-file (out filename :direction :output :if-exists :supersede)
        (with-standard-io-syntax
          (print *db* out))))
    
    (defun load-db (filename)
      (with-open-file (in filename)
        (with-standard-io-syntax
          (setf *db* (read in)))))

    查询数据库 🔍

    有了数据库,我们当然需要查询的功能。我们可以使用remove-if-not函数来筛选出符合条件的记录:

    (defun select (selector-fn)
      (remove-if-not selector-fn *db*))
    
    (defun artist-selector (artist)
      #'(lambda (cd) (equal (getf cd :artist) artist)))

    调用示例:

    (select (artist-selector "Kathy Mattea"))

    更新和删除记录 ✂️

    为了使数据库更加灵活,我们还可以添加更新和删除的功能。这可以通过mapcarremove-if等函数实现:

    (defun update (selector-fn &key title artist rating (ripped nil ripped-p))
      (setf *db*
            (mapcar
             #'(lambda (row)
                 (when (funcall selector-fn row)
                   (if title (setf (getf row :title) title))
                   (if artist (setf (getf row :artist) artist))
                   (if rating (setf (getf row :rating) rating))
                   (if ripped-p (setf (getf row :ripped) ripped)))
                 row) *db*)))
    
    (defun delete-rows (selector-fn)
      (setf *db* (remove-if selector-fn *db*)))
    

    update 函数的逐行解析

    这段代码定义了一个 update 函数,用于更新数据库 *db* 中符合条件的CD记录。该函数使用一个选择器函数(selector-fn)来匹配需要更新的记录,并通过关键字参数提供更新的内容。我们将逐行解释它的功能和关键点。


    (defun update (selector-fn &key title artist rating (ripped nil ripped-p))
    • 功能: update 函数的目的是更新数据库中的CD记录。
    • 参数:
    • selector-fn: 一个函数,用于选择需要更新的记录。该函数会传递给 funcall,并对每个CD记录进行过滤。
    • &key: 关键字参数,允许用户指定哪些属性需要更新。包括:
      • title: 新的标题(如果提供)。
      • artist: 新的艺术家名称(如果提供)。
      • rating: 新的评分(如果提供)。
      • (ripped nil ripped-p): 是否已翻录。ripped-p 是一个标志,表示是否传递了 ripped 参数。默认值为 nil
    • 要点:
    • &key 用于定义关键字参数,这在Lisp中是一种便捷的方式来传递可选参数。
    • (ripped nil ripped-p) 定义了一个特殊的关键字参数 ripped,并通过 ripped-p 检查该参数是否实际被传递。

      (setf *db*
        (mapcar
         #'(lambda (row)
             (when (funcall selector-fn row)
               (if title (setf (getf row :title) title))
               (if artist (setf (getf row :artist) artist))
               (if rating (setf (getf row :rating) rating))
               (if ripped-p (setf (getf row :ripped) ripped)))
             row) *db*)))
    • 功能: 该部分的核心是使用 mapcar 函数来遍历 *db* 中的每一条CD记录,并对每一条记录进行更新。如果某条记录符合 selector-fn 的选择条件,则根据传入的关键字参数更新相应的字段。
    • 详细解释:
    1. setf *db*:
      • setf 用于修改全局变量 *db*,将其设置为 mapcar 函数的结果。mapcar 的作用是对 *db* 列表中的每一条记录应用指定的函数。
    2. mapcar:
      • mapcar 用于遍历 *db* 列表,并对每条记录 row 应用一个匿名函数(lambda)。
      • 每次迭代都会将当前的 row 传递给匿名函数,匿名函数根据条件选择是否更新该记录。
    3. lambda 函数:
      • 匿名函数(lambda)接收每一条记录 row,并通过 funcall 调用 selector-fn 来决定是否对该记录进行更新。
      • selector-fn 是一个选择器函数,它接收 row 作为参数,返回 tnil,表示是否需要更新该记录。
    4. when (funcall selector-fn row):
      • funcall 调用 selector-fn,并将 row 作为参数传递。如果 selector-fn 对该 row 返回 t,则进入 when 语句块,执行更新操作。
    5. 属性更新操作:
      • if title (setf (getf row :title) title):
      • 如果 title 参数不为 nil,则使用 setf 更新该记录的 :title 属性为新的 title 值。
      • if artist (setf (getf row :artist) artist):
      • 类似地,如果 artist 参数不为 nil,则更新该记录的 :artist 属性。
      • if rating (setf (getf row :rating) rating):
      • 如果 rating 参数不为 nil,则更新该记录的 :rating 属性。
      • if ripped-p (setf (getf row :ripped) ripped):
      • 如果 ripped-pt,表示用户传递了 ripped 参数,则更新该记录的 :ripped 属性。注意这里是通过 ripped-p 来判断是否传递了 ripped 参数,而不是直接判断 ripped 的值。这允许用户显式地将 ripped 设置为 nilt
    6. row 的返回:
      • 无论记录是否被更新,lambda 函数都会返回 row,并将其包含在新的列表中。mapcar 将这些记录组成一个新的数据库列表。

    关键点和难点解析

    • 使用 selector-fn 进行条件选择:
    • 该函数的灵活性体现在它允许用户通过一个选择器函数(selector-fn)来指定需要更新的记录。这个设计非常通用,用户可以传递任意的选择逻辑,从而实现复杂的过滤条件。
    • 关键字参数的使用:
    • 关键字参数(&key)使得函数调用更加灵活。用户可以选择只更新某几个字段,而不必传递所有字段的值。
    • (ripped nil ripped-p) 是一种较为高级的用法,它不仅允许用户传递 ripped 值,还可以检测用户是否传递了这个参数。这使得 ripped 可以显式地设置为 nilt
    • mapcar 的使用:
    • mapcar 是一个常用的函数式编程工具,用于对列表中的每个元素应用一个函数,并返回一个新列表。这里它被用来遍历 *db*,并返回一个更新后的数据库。
    • setfgetf 的使用:
    • setf 是Lisp中的通用赋值操作符,用于修改数据结构中的值。在这里,它被用来更新属性列表(plist)中的值。
    • getf 用于从属性列表中获取指定键的值。通过 setfgetf 的组合,可以修改属性列表的某个键值对。

    示例

    假设数据库 *db* 中有以下CD记录:

    (setq *db* (list (make-cd "Title1" "Artist1" 5 t)
                     (make-cd "Title2" "Artist2" 4 nil)
                     (make-cd "Title3" "Artist3" 3 t)))

    现在,我们希望将所有评分为 5 的CD的标题更新为 "New Title",可以这样调用 update 函数:

    (update (where :rating 5) :title "New Title")

    调用后,数据库中的第一条记录的标题将被更新为 "New Title",而其他记录保持不变。


    结论 🎊

    通过上述步骤,我们用Lisp成功实现了一个简单的数据库,能够存储、查询、更新和删除CD记录。尽管Lisp的语法可能让初学者感到陌生,但它的优雅与强大在实际应用中会让你感受到编程的乐趣。希望你在探索Lisp的旅程中,能够感受到这位优雅舞伴所带来的无限魅力。

    参考文献 📚

    1. 编程之禅. (2021). 用 Lisp 实现一个简单的数据库. Retrieved from 编程之禅
    (defun make-cd (title artist rating ripped)
      (list :title title :artist artist :rating rating :ripped ripped))
    
    (defvar *db* nil)
    
    (defun add-record (cd) (push cd *db*))
    
    (defun dump-db ()
      (dolist (cd *db*)
        (format t "~{~a:~10t~a~%~}~%" cd)))
    
    
    (defun prompt-read (prompt)
      (format *query-io* "~a: " prompt)
      (force-output *query-io*)
      (read-line *query-io*))
    
    (defun prompt-for-cd ()
      (make-cd
       (prompt-read "Title")
       (prompt-read "Artist")
       (OR (parse-integer (prompt-read "Rating") :junk-allowed t) 0)
       (y-or-n-p "Ripped [y/n]: ")))
    
    (defun add-cds ()
      (loop (add-record (prompt-for-cd))
    	(if (not (y-or-n-p "Another?[y/n]: ")) (return))))
    
    (defun save-db (filename)
      (with-open-file (out filename
    		       :direction :output
    		       :if-exists :supersede)
        (with-standard-io-syntax
          (print *db* out))))
    
    
    (defun load-db (filename)
      (with-open-file (in filename)
        (with-standard-io-syntax
          (setf *db* (read in)))))
    
    (defun select (selector-fn)
      (remove-if-not selector-fn *db*))
    
    (defun where (&key title artist rating (ripped nil ripped-p))
      #'(lambda (cd)
          (and
           (if title (equal (getf cd :title) title) t)
           (if artist (equal (getf cd :artist) artist) t)
           (if rating (equal (getf cd :rating) rating) t)
           (if ripped-p (equal (getf cd :ripped) ripped) t))))
    
    (defun update (selector-fn &key title artist rating (ripped nil ripped-p))
      (setf *db*
    	(mapcar
    	 #'(lambda (row)
    	     (when (funcall selector-fn row)
    	       (if title (setf (getf row :title) title))
    	       (if artist (setf (getf row :artist) artist))
    	       (if rating (setf (getf row :rating) rating))
    	       (if ripped-p (setf (getf row :ripped) ripped)))
    	     row) *db*)))
    
    (defun delete-rows (selector-fn)
      (setf *db* (remove-if selector-fn *db*)))

    这段代码实现了一个简单的CD数据库管理系统,可以添加、查询、更新和删除CD记录,并支持数据库的加载和保存。我们将逐步分析每一个部分的功能和关键点。


    1. make-cd: 创建 CD 记录

    (defun make-cd (title artist rating ripped)
      (list :title title :artist artist :rating rating :ripped ripped))
    • 功能: make-cd 函数用于创建一个新的CD记录。每个CD记录是一个列表,包含标题(:title)、艺术家(:artist)、评分(:rating)和是否已翻录(:ripped)。
    • 要点:
    • 使用 list 函数创建一个属性列表(property list,简称 plist),其键(以冒号开头,如 :title)是符号,值是函数参数。
    • 属性列表在Lisp中是一种常见的数据结构,使用 getf 函数可以访问键对应的值。

    2. *db*: 数据库变量

    (defvar *db* nil)
    • 功能: 这是一个全局变量,用于存储所有CD记录。最初它被设置为 nil,表示数据库为空。
    • 要点:
    • *db* 是一个全局变量,遵循Common Lisp中全局变量的命名习惯,即用星号包围变量名。
    • 使用 defvar 定义全局变量,如果该变量已经存在,defvar 不会重新初始化。

    3. add-record: 添加记录到数据库

    (defun add-record (cd) (push cd *db*))
    • 功能: 将一个CD记录添加到数据库中。
    • 要点:
    • 使用 push 将CD记录添加到 *db* 列表的头部。push 是一个高效的操作,因为它直接修改列表的指向。

    4. dump-db: 打印数据库内容

    (defun dump-db ()
      (dolist (cd *db*)
        (format t "~{~a:~10t~a~%~}~%" cd)))
    • 功能: 遍历 *db*,逐条打印每个CD记录的属性和值。
    • 要点:
    • dolist 用于遍历数据库中的每个CD记录。
    • format 函数用于格式化输出。这里的 ~{~a:~10t~a~%~} 格式化字符串用于按键值对的形式打印CD的元数据。

    5. prompt-read: 从用户输入读取数据

    (defun prompt-read (prompt)
      (format *query-io* "~a: " prompt)
      (force-output *query-io*)
      (read-line *query-io*))
    • 功能: 提示用户输入信息,并读取用户输入的字符串。
    • 要点:
    • format 用于显示提示信息。
    • force-output 确保提示信息立即输出,而 read-line 则从用户输入中读取一行文本。

    6. prompt-for-cd: 提示用户输入CD信息

    (defun prompt-for-cd ()
      (make-cd
       (prompt-read "Title")
       (prompt-read "Artist")
       (or (parse-integer (prompt-read "Rating") :junk-allowed t) 0)
       (y-or-n-p "Ripped [y/n]: ")))
    • 功能: 提示用户输入CD的详细信息,并创建并返回一个CD记录。
    • 要点:
    • 使用 prompt-read 函数提示用户输入标题和艺术家。
    • parse-integer 将用户输入的评分转换为整数,如果输入的内容不是有效的数字,则返回 0(使用了 :junk-allowed 选项)。
    • 使用 y-or-n-p 确认CD是否已经翻录。它是一个用于读取 y/n 回答的函数。

    7. add-cds: 循环添加多个CD记录

    (defun add-cds ()
      (loop (add-record (prompt-for-cd))
        (if (not (y-or-n-p "Another?[y/n]: ")) (return))))
    • 功能: 支持用户通过循环方式添加多个CD记录,直到用户选择停止。
    • 要点:
    • loop 用于无限循环,直到用户选择不再添加CD。
    • 每次循环调用 add-record 添加新CD,并通过 y-or-n-p 确认是否继续。

    8. save-db: 保存数据库到文件

    (defun save-db (filename)
      (with-open-file (out filename
                   :direction :output
                   :if-exists :supersede)
        (with-standard-io-syntax
          (print *db* out))))
    • 功能: 将数据库保存到指定文件中。
    • 要点:
    • with-open-file 打开一个文件进行输出,:if-exists :supersede 表示如果文件存在则覆盖。
    • with-standard-io-syntax 设置标准的I/O语法,确保数据结构可以正确地读/写。
    • print 将数据库的内容输出到文件。

    9. load-db: 从文件加载数据库

    (defun load-db (filename)
      (with-open-file (in filename)
        (with-standard-io-syntax
          (setf *db* (read in)))))
    • 功能: 从指定文件中加载数据库。
    • 要点:
    • save-db 类似使用 with-open-file 打开文件进行读取。
    • read 函数从文件中读取数据库数据,并使用 setf 将其赋值给 *db*

    10. select: 选择符合条件的CD记录

    (defun select (selector-fn)
      (remove-if-not selector-fn *db*))
    • 功能: 选择所有满足 selector-fn 条件的CD记录。
    • 要点:
    • remove-if-not 用于过滤列表,保留所有满足 selector-fn 函数返回 true 的元素。

    11. where: 构造选择条件

    (defun where (&key title artist rating (ripped nil ripped-p))
      #'(lambda (cd)
          (and
           (if title (equal (getf cd :title) title) t)
           (if artist (equal (getf cd :artist) artist) t)
           (if rating (equal (getf cd :rating) rating) t)
           (if ripped-p (equal (getf cd :ripped) ripped) t))))
    • 功能: 根据给定的键(如 titleartistrating 等)构造一个选择函数,用于在 select 函数中进行过滤。
    • 要点:
    • &key 允许参数按关键字传递。
    • 返回一个匿名函数(lambda),该函数根据传递的条件对每个CD记录进行匹配。

    12. update: 更新符合条件的CD记录

    (defun update (selector-fn &key title artist rating (ripped nil ripped-p))
      (setf *db*
        (mapcar
         #'(lambda (row)
             (when (funcall selector-fn row)
               (if title (setf (getf row :title) title))
               (if artist (setf (getf row :artist) artist))
               (if rating (setf (getf row :rating) rating))
               (if ripped-p (setf (getf row :ripped) ripped)))
             row) *db*)))
    • 功能: 更新数据库中所有符合 selector-fn 条件的CD记录。
    • 要点:
    • 使用 mapcar 遍历 *db*,对每个符合条件的CD记录进行更新。
    • setf 更新属性列表中的对应属性。

    13. delete-rows: 删除符合条件的CD记录

    • 功能: 删除所有满足 selector-fn 条件的CD记录。
    • 要点:
    • remove-if 移除列表中符合条件的元素。

    (defun delete-rows (selector-fn)
      (setf *db* (remove-if selector-fn *db*)))

    总结

    这段代码实现了一个简单的CD数据库系统,使用Common Lisp的基本数据结构和控制流来管理数据。代码的关键点包括:

    • 使用属性列表存储CD记录。
    • 提供了增删改查的操作。
    • 使用函数式编程风格(如 selector-fn)进行选择和过滤。

    通过这些功能,用户可以灵活地管理和操作CD数据库。

  • Common Lisp通过CFFI操作mmap

    ;; Load CFFI if not already loaded
    (ql:quickload :cffi)
    
    ;; Define mmap-prot enum
    (cffi:defcenum mmap-prot
      (:none 0)
      (:read 1)
      (:write 2)
      (:read-write 3))
    
    ;; Define mmap-struct structure
    (cffi:defcstruct mmap-struct
      (addr :pointer)
      (length :size)
      (prot :int)
      (flags :int))
    
    ;; Create mmap-struct instance
    (defun create-mmap-struct ()
      (cffi:foreign-alloc '(:struct mmap-struct)))
    
    ;; Set mmap-struct fields
    (defun set-mmap-struct-fields (mmap-struct)
      (setf (cffi:foreign-slot-value mmap-struct '(:struct mmap-struct) 'addr) (cffi:foreign-alloc :pointer))
      (setf (cffi:foreign-slot-value mmap-struct '(:struct mmap-struct) 'length) 4096)
      (setf (cffi:foreign-slot-value mmap-struct '(:struct mmap-struct) 'prot) (cffi:foreign-enum-value 'mmap-prot :read))
      (setf (cffi:foreign-slot-value mmap-struct '(:struct mmap-struct) 'flags) 0))
    
    ;; Usage example
    (let ((mmap (create-mmap-struct)))
      (set-mmap-struct-fields mmap)
      ;; You can now use mmap for further operations...
      ;; For example, print the fields
      (format t "Address: ~A~%" (cffi:foreign-slot-value mmap '(:struct mmap-struct) 'addr))
      (format t "Length: ~A~%" (cffi:foreign-slot-value mmap '(:struct mmap-struct) 'length))
      (format t "Protection: ~A~%" (cffi:foreign-slot-value mmap '(:struct mmap-struct) 'prot))
      (format t "Flags: ~A~%" (cffi:foreign-slot-value mmap '(:struct mmap-struct) 'flags)))

    让我们详细解析上面的 Common Lisp 代码。

    1. 加载 CFFI 库

    (ql:quickload :cffi)
    • ql:quickload:这个函数属于 Quicklisp,它用于加载指定的库。在这里,加载的是 CFFI(Common Foreign Function Interface),一个用于与 C 库交互的库。
    • :cffi:cffiCFFI 库的名称。通过 ql:quickload,如果 CFFI 还没有安装,它将自动下载并加载它。

    2. 定义枚举类型 mmap-prot

    (cffi:defcenum mmap-prot
      (:none 0)
      (:read 1)
      (:write 2)
      (:read-write 3))
    • cffi:defcenum:CFFI 的一个宏,用于定义 C 风格的枚举类型。
    • mmap-prot:枚举的名称,表示内存映射的保护模式。
    • (:none 0):定义枚举的第一个值,:none 对应的值是 0
    • (:read 1):read 表示读权限,对应的值是 1
    • (:write 2):write 表示写权限,对应的值是 2
    • (:read-write 3):read-write 表示读写权限,对应的值是 3

    此枚举类型将用于指示内存映射的权限。

    3. 定义 C 结构体 mmap-struct

    (cffi:defcstruct mmap-struct
      (addr :pointer)
      (length :size)
      (prot :int)
      (flags :int))
    • cffi:defcstruct:定义一个 C 结构体。
    • mmap-struct:结构体的名称。
    • (addr :pointer)addr 是结构体中的第一个字段,它的类型是 :pointer,表示这是一个指针。
    • (length :size)length 是结构体中的第二个字段,类型为 :size,表示一个大小(通常是无符号整数类型)。
    • (prot :int)prot 是结构体中的第三个字段,类型为 :int,表示一个整数,通常用于存储保护权限。
    • (flags :int)flags 是结构体中的第四个字段,类型为 :int,表示标志位。

    这个结构体类似于 C 中的结构体,通常用于存储内存映射相关的信息。

    4. 创建 mmap-struct 实例

    (defun create-mmap-struct ()
      (cffi:foreign-alloc '(:struct mmap-struct)))
    • defun:定义一个函数,函数名是 create-mmap-struct
    • cffi:foreign-alloc:这个函数用于在 C 的堆上分配内存。它会分配可以存储 mmap-struct 结构体的内存。
    • '(:struct mmap-struct):指定分配的内存类型是一个 mmap-struct 结构体。

    这个函数返回的是一个指向分配的内存的指针,该指针将被我们用来访问和操作 mmap-struct 结构体的字段。

    5. 设置 mmap-struct 的字段

    (defun set-mmap-struct-fields (mmap-struct)
      (setf (cffi:foreign-slot-value mmap-struct '(:struct mmap-struct) 'addr) (cffi:foreign-alloc :pointer))
      (setf (cffi:foreign-slot-value mmap-struct '(:struct mmap-struct) 'length) 4096)
      (setf (cffi:foreign-slot-value mmap-struct '(:struct mmap-struct) 'prot) (cffi:foreign-enum-value 'mmap-prot :read))
      (setf (cffi:foreign-slot-value mmap-struct '(:struct mmap-struct) 'flags) 0))
    • defun:定义一个函数,函数名是 set-mmap-struct-fields
    • mmap-struct:这是函数的参数,表示 mmap-struct 结构体的指针。
    • setfsetf 是 Lisp 的赋值操作符,用于设置某个位置的值。
    • cffi:foreign-slot-value:这个函数用于访问或修改 C 结构体的字段。
      • 第一个参数是 mmap-struct,表示结构体的指针。
      • 第二个参数是 (:struct mmap-struct),它指定了结构体的类型。
      • 第三个参数是字段的名称,如 'addr'length'prot'flags
    • (cffi:foreign-alloc :pointer):分配一个指针类型的内存块,并将它赋值给 addr 字段。
    • 4096:将 4096 赋值给 length 字段,这通常表示内存映射的大小。
    • (cffi:foreign-enum-value 'mmap-prot :read):获取枚举类型 mmap-prot:read 对应的值(即 1),并将它赋值给 prot 字段。
    • 0:将 0 赋值给 flags 字段。

    这个函数的作用是:通过传入一个 mmap-struct 的指针,给它的各个字段赋值。

    6. 使用示例

    (let ((mmap (create-mmap-struct)))
      (set-mmap-struct-fields mmap)
      ;; 你现在可以使用 mmap 进行其他操作
      ;; 例如,打印字段的值
      (format t "Address: ~A~%" (cffi:foreign-slot-value mmap '(:struct mmap-struct) 'addr))
      (format t "Length: ~A~%" (cffi:foreign-slot-value mmap '(:struct mmap-struct) 'length))
      (format t "Protection: ~A~%" (cffi:foreign-slot-value mmap '(:struct mmap-struct) 'prot))
      (format t "Flags: ~A~%" (cffi:foreign-slot-value mmap '(:struct mmap-struct) 'flags)))
    • let:创建一个局部变量 mmap,并将 create-mmap-struct 的返回值赋值给它。这里 mmap 是指向 mmap-struct 结构体的指针。
    • (set-mmap-struct-fields mmap):调用 set-mmap-struct-fields,设置 mmap 结构体的字段。
    • formatformat 是 Lisp 中用于字符串格式化和输出的函数。
    • t:表示输出到标准输出(通常是控制台)。
    • "Address: ~A~%":输出的字符串模板,~A 是一个占位符,表示通用输出(可以是任何类型的对象),~% 表示换行符。
    • cffi:foreign-slot-value:从 mmap 中读取各个字段的值,分别是 addrlengthprotflags,并输出它们的值。

    运行结果:

    • Address:输出结构体中的 addr 字段的值,这是一个指针。
    • Length:输出 length 字段的值,即 4096
    • Protection:输出 prot 字段的值,即 1(代表 :read 权限)。
    • Flags:输出 flags 字段的值,即 0

    总结

    • 我们首先定义了一个 mmap-struct 结构体,该结构体包含了指针、长度、权限和标志字段。
    • 然后,我们创建了一个函数 create-mmap-struct 来分配该结构体的内存。
    • 通过 set-mmap-struct-fields 函数,我们设置了结构体的各个字段。
    • 最后,在 let 块中,我们创建并初始化了一个结构体实例,并打印其各个字段的值。

    这段代码展示了如何使用 CFFI 在 Lisp 中定义并操作一个 C 风格的结构体。

  • 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

    总结

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

  • 🌿 探索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 (歌词引用)
  • 人生梦想 - 关注前沿的计算机技术 acejoy.com