标签: Lisp

  • 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 中有许多其他特殊用途,包括字符、向量、函数引用等。

  • 🌪️ 旋转与循环:在Lisp的世界里翩翩起舞

    在这个充满无限可能的编程宇宙里,Lisp就像是一位古老的舞者,以优雅的身姿在功能与数据之间自由穿梭。今天,我们将深入探讨Lisp中的闭包、循环和字符串处理,带你领略这位舞者如何在代码的舞台上翩翩起舞。

    🌍 生命的循环:闭包与作用域

    生活中,循环往往让人感到乏味,但在Lisp的元宇宙里,循环则是行动的象征,而函数则是通往内心世界的窗口。首先,我们要理解作用域的概念——就如一位演员在舞台上,只能在特定的时间和空间内展现其才华。

    全局符号的作用域使得Lisp图像的任何部分都可以达到或修改其值,而其他符号则拥有词法作用域。这意味着这些符号只在被定义的文本块中有效,当文本块结束时,它们不再与原值绑定。比如,参数符号只在函数体内与调用函数的参数绑定,直到函数返回为止。以下是一个简单的示例:

    (defun my-func (param1 param2)
      (let ((sum (+ param1 param2)))
        (print sum)))

    在这个函数中,param1param2的作用域是整个函数体,而sum的作用域则只限于let块内部。这就像一位舞者在舞台上,随着音乐的节奏,只有在特定的旋律中才能展现自己的风采。

    🔍 闭包:锁住环境的魔法

    在Lisp中,我们可以定义局部符号,使用letlet*来实现。这就像是在舞台上为每位演员准备了自己的道具,确保他们在表演时能够随心所欲。更神奇的是,Lisp允许我们返回一个函数,形成闭包,这样即使源作用域已经结束,定义时的环境依然与函数相伴而生。

    看看这个“工厂”函数,它为我们生产加法器:

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

    在这个例子中,my-adder函数返回一个闭包,能够记住它的环境,保持sum的状态。你可以创建多个加法器,每个加法器都拥有自己的“记忆”:

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

    这就像是每位舞者都有自己的节奏与风格,尽管在同一个舞台上表演,却展现出不同的魅力。

    🔄 循环:编程世界的舞步

    在Lisp中,控制流的另一大类便是循环。我们可以使用多种形式的循环,从简单的loop到更复杂的dotimesdolist。想象一下,舞者在舞台上如何随着音乐的节拍反复舞动。

    🎶 永恒的循环

    在Lisp中,loop可以形成一个永恒的循环,直到调用return

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

    这段代码就像是在舞台上不停地转圈,直到你决定停下来的那一刻。

    🔢 固定次数的循环

    dotimes允许我们重复执行某个动作固定次数:

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

    想象一下,舞者在舞台上跳了十次相同的舞步,观众们不禁为之喝彩。

    📜 遍历的循环

    dolist则是将每个元素一一展现,如同舞者在每个观众面前展示各自的风采:

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

    在这里,每一个数字都是一个舞者,依次走上舞台,展现他们的魅力。

    🧵 字符串:编程中的语言之舞

    在Lisp中,字符串用双引号括起来,形成一个字符的一维数组。Lisp提供了多种字符串操作符,帮助我们在字符的世界中自由舞动。

    字符串的魔法

    常见的字符串操作包括:

    • (length x):获取字符串长度
    • (string= x y):比较两个字符串的相等性
    • (string-upcase x):将字符串转换为大写
    • (reverse x):反转字符串内容

    这些操作就像是舞者在舞台上的各种姿态,灵活多变,令人目不暇接。

    🎭 字符与字符串的转换

    在Lisp中,字符用#\表示,字符常量也可以用类似的方式定义:

    #\space ; 表示空格字符

    通过这些表达,我们可以在字符串与字符之间自由转换,仿佛舞者能够在不同的舞蹈风格之间自如切换。

    📚 结论:在Lisp的舞台上畅游

    在Lisp的世界中,闭包、循环与字符串处理如同舞蹈的各个元素,彼此交织,形成了一幅绚丽的编程画卷。每一个功能都是舞者的舞步,每一行代码都是他们的旋律。在这个舞台上,我们不仅能看到代码的美丽,更能感受到编程的乐趣。

    📝 参考文献

    1. Common Lisp – The Tutorial Part 7
    2. https://github.com/rabbibotton/clog/blob/main/LEARN.md

    面向记忆的学习材料

    任务目标

    快速学习并记住Common Lisp中关于闭包、循环和字符串处理的重要概念。

    知识点: 作用域和闭包
    题目: 在Common Lisp中,以下哪种符号具有词法作用域(lexical scope)?
    选项:
    A. 全局符号
    B. 函数参数
    C. 特殊变量
    D. 宏参数

    正确答案: B
    解析: 在Common Lisp中,全局符号具有全局作用域,可以在整个Lisp镜像中访问或修改。而函数参数等其他符号具有词法作用域,仅在其定义的文本块内有效。函数参数的绑定仅在函数体内有效,直到函数返回。
    速记提示: “函数参数,局部有效”

    知识点: let和let*的使用

    题目: 以下哪种情况下应该使用let*而不是let?
    选项:
    A. 当需要定义多个相互独立的局部变量时
    B. 当一个定义需要引用另一个定义时
    C. 当只需要定义一个局部变量时
    D. 当定义的变量都是全局的时

    正确答案: B
    解析: let用于创建局部变量,但如果一个定义需要引用另一个定义,就应该使用let。let*允许后面的定义引用前面的定义,而let中的所有定义是并行的,不能相互引用。
    速记提示: “let*允许相互引用”

    知识点: 闭包的概念
    题目: 在Common Lisp中,什么是闭包(closure)?
    选项:
    A. 一种特殊的全局变量
    B. 一种循环结构
    C. 一个带有其定义环境的函数
    D. 一种字符串处理方法

    正确答案: C
    解析: 闭包是一个函数,它携带了其定义时的环境。即使函数的源代码范围已经词法关闭(最后的右括号已闭合),定义该函数的环境仍然与之一起存在。这就是为什么它被称为”闭包”。
    速记提示: “函数+环境=闭包”

    知识点: loop..return循环
    题目: 在Common Lisp中,如果loop..return结构中从未调用return,会发生什么?
    选项:
    A. 循环会立即终止
    B. 循环会执行一次后终止
    C. 循环会无限执行
    D. 会产生编译错误

    正确答案: C
    解析: 在Common Lisp中,如果loop..return结构中从未调用return,循环将永远执行下去。return用于提供退出循环的条件,如果没有这个条件,循环就会一直进行。
    速记提示: “无return,永远loop”

    知识点: dotimes循环
    题目: 在Common Lisp的dotimes循环中,循环变量n的取值范围是什么?
    选项:
    A. 1 到 n
    B. 0 到 n
    C. 0 到 n-1
    D. 1 到 n-1

    正确答案: C
    解析: 在dotimes循环中,循环变量n的取值范围是0到n-1。循环重复n次,每次循环n等于0, 1, 2, …, 直到n-1。
    速记提示: “dotimes从0到n-1”

    知识点: dolist循环
    题目: 在Common Lisp的dolist循环中,循环变量n是如何变化的?
    选项:
    A. n每次增加1
    B. n每次减少1
    C. n绑定到列表的下一个元素
    D. n保持不变

    正确答案: C
    解析: 在dolist循环中,每次循环n都会绑定到列表的下一个元素,直到所有元素都被处理完毕。这使得dolist非常适合遍历列表中的所有元素。
    速记提示: “dolist绑定下一元素”

    知识点: do循环
    题目: Common Lisp中的do循环有什么特点?
    选项:
    A. 只能进行单一循环
    B. 只能处理数字
    C. 允许同时进行多个循环
    D. 必须使用关键字loop

    正确答案: C
    解析: Common Lisp中的do循环允许同时进行多个循环。每个循环都可以定义为(符号 起始值 如何改变符号)的形式。这使得do循环非常灵活,能够处理复杂的循环逻辑。
    速记提示: “do支持多循环并行”

    知识点: 字符串创建
    题目: 在Common Lisp中,如何创建字符串?
    选项:
    A. 使用单引号
    B. 使用双引号
    C. 使用反引号
    D. 使用括号

    正确答案: B
    解析: 在Common Lisp中,字符串是使用双引号创建的。例如,”Hello, World!”就是一个字符串。字符串在Lisp中被视为字符的一维数组。
    速记提示: “双引号创建字符串”

    知识点: 字符串操作
    题目: 以下哪个函数用于获取字符串的长度?
    选项:
    A. string-length
    B. str-len
    C. length
    D. size

    正确答案: C
    解析: 在Common Lisp中,length函数用于获取字符串的长度。这个函数不仅可以用于字符串,还可以用于其他序列类型。使用方法为(length x),其中x是要测量长度的字符串。
    速记提示: “length测长度”

    知识点: 字符串比较
    题目: Common Lisp中用于比较字符串相等性的函数是?
    选项:
    A. string=
    B. equal
    C. eq
    D. eql

    正确答案: A
    解析: 在Common Lisp中,string=函数用于比较两个字符串是否相等。这是一个区分大小写的比较。使用方法为(string= x y),其中x和y是要比较的字符串。
    速记提示: “string=比较字符串”

    知识点: 字符串大小写转换
    题目: 如何将字符串转换为大写?
    选项:
    A. string-uppercase
    B. string-upcase
    C. upcase-string
    D. to-upper

    正确答案: B
    解析: 在Common Lisp中,string-upcase函数用于将字符串转换为大写。它返回一个新的大写字符串,而不修改原始字符串。使用方法为(string-upcase x),其中x是要转换的字符串。
    速记提示: “string-upcase变大写”

    知识点: 字符串修剪
    题目: 哪个函数用于去除字符串两端的空白字符?
    选项:
    A. string-trim
    B. string-strip
    C. trim-string
    D. remove-whitespace

    正确答案: A
    解析: 在Common Lisp中,string-trim函数用于去除字符串两端的空白字符。它返回一个新的已修剪的字符串。使用方法为(string-trim ” ” x),其中x是要修剪的字符串,第一个参数指定要修剪的字符(这里是空格)。
    速记提示: “string-trim去两端”

    知识点: 字符串连接
    题目: 在Common Lisp中,如何连接多个字符串?
    选项:
    A. string-append
    B. concat
    C. concatenate
    D. join

    正确答案: C
    解析: 在Common Lisp中,concatenate函数用于连接多个字符串。使用方法为(concatenate ‘string x y),其中x和y是要连接的字符串。注意第一个参数’string指定了结果的类型。
    速记提示: “concatenate连接字符串”

    知识点: 字符到字符串的转换
    题目: 如何将一个字符转换为字符串?
    选项:
    A. char-to-string
    B. make-string
    C. string
    D. to-string

    正确答案: C
    解析: 在Common Lisp中,string函数用于将一个字符转换为字符串。使用方法为(string #\x),其中#\x表示要转换的字符。这个函数会返回一个包含该字符的新字符串。
    速记提示: “string函数字符变字符串”

    知识点: 字符串解析为数字
    题目: 哪个函数用于将表示整数的字符串转换为数字?
    选项:
    A. string-to-number
    B. parse-integer
    C. read-number
    D. to-integer

    正确答案: B
    解析: 在Common Lisp中,parse-integer函数用于将表示整数的字符串转换为数字。使用方法为(parse-integer “123”),它会返回对应的整数值。这个函数只能处理整数,不能处理浮点数。
    速记提示: “parse-integer字符串变整数”

    知识点: 数据到字符串的转换
    题目: 如何将任意Lisp对象转换为其打印表示的字符串?
    选项:
    A. object-to-string
    B. print-to-string
    C. write-to-string
    D. stringify

    正确答案: C
    解析: 在Common Lisp中,write-to-string函数用于将任意Lisp对象转换为其打印表示的字符串。使用方法为(write-to-string x),其中x是要转换的对象。这个函数会返回一个新的字符串,表示该对象的打印形式。
    速记提示: “write-to-string对象变字符串”

    知识点: 字符表示
    题目: 在Common Lisp中,如何表示一个字符?
    选项:
    A. ‘c
    B. “c”
    C. #\c
    D. \c

    正确答案: C
    解析: 在Common Lisp中,字符使用#\加上字符本身来表示。例如,#\z表示字符’z’。这种表示方法允许明确地指定单个字符,而不是字符串或符号。
    速记提示: “#\表示单个字符”

    知识点: 字符操作
    题目: 哪个函数用于获取字符串中特定位置的字符?
    选项:
    A. string-char
    B. char
    C. get-char
    D. elt

    正确答案: B
    解析: 在Common Lisp中,char函数用于获取字符串中特定位置的字符。使用方法为(char my-string x),其中my-string是字符串,x是要获取的字符的索引(从0开始)。这个函数返回指定位置的字符。
    速记提示: “char获取特定字符”

    知识点: 字符比较
    题目: 用于比较两个字符是否相等的函数是?
    选项:
    A. char-equal
    B. char=
    C. equal-char
    D. eq

    正确答案: B
    解析: 在Common Lisp中,char=函数用于比较两个字符是否相等。这是一个区分大小写的比较。使用方法为(char= x y),其中x和y是要比较的字符。如果两个字符相等,函数返回true。
    速记提示: “char=比较字符相等”

    知识点: 字符常量
    题目: 以下哪个不是Common Lisp中预定义的字符常量?
    选项:
    A. #\space
    B. #\newline
    C. #\tab
    D. #\comma

    正确答案: D
    解析: Common Lisp预定义了一些特殊的字符常量,包括#\space(空格)、#\newline(换行)、#\tab(制表符)等。但#\comma(逗号)不是预定义的字符常量。其他常用的字符常量还包括#\return(回车)、#\backspace(退格)等。
    速记提示: “常用特殊字符有预定义常量”

    总结

    本学习材料涵盖了Common Lisp中关于闭包、循环和字符串处理的重要概念。主要内容包括:

    1. 作用域和闭包:理解全局作用域和词法作用域的区别,以及闭包的概念和应用。
    2. 循环结构:介绍了loop..return、dotimes、dolist和do等不同类型的循环,每种循环都有其特定的用途和语法。
    3. 字符串操作:包括字符串的创建、比较、修改、连接等基本操作,以及字符串与其他数据类型之间的转换。
    4. 字符处理:讲解了字符的表示方法,以及相关的操作函数。

    掌握这些概念对于使用Common Lisp进行编程至关重要。建议读者多加练习,特别是在实际编程中运用这些概念,以加深理解。


  • 🌟 探索Lisp的魅力:全局变量与列表的魔力

    在编程的世界里,Lisp就像一位神秘的武术大师,教我们如何在代码与数据之间游刃有余。正如电影《功夫熊猫》中阿宝所学到的那样:“蜡在右手,蜡在左手,蜡在上,蜡在下”,每一个动作都是为了掌握更深的技巧。今天,我们就跟随Lisp的脚步,深入探讨全局变量和列表的奥秘。

    🧙‍♂️ Lisp的神奇:代码即数据

    Lisp的名字来源于“List Processing”,其核心思想就是通过列表来处理数据。你可以把Lisp视作一个魔法师,它允许你将代码视为数据,反之亦然。这种特性被称为“同构性”(homoiconicity),是其他编程语言所无法企及的。通过这种强大的能力,你将能够以独特的方式操控程序。

    ⚔️ 恶魔般的全局变量

    在这个充满力量的世界中,全局变量就像是双刃剑,既能为你带来便利,也可能让你陷入危险的境地。全局变量的定义如下:

    (defvar *symbol-for-global-variable* "optional initial value" "I LOVE DOC strings")

    这里的*my-var*是我们定义全局变量的惯例,耳罩样式的星号告诉我们:“小心!我有可能是个危险分子!”为什么说全局变量有“恶魔潜力”呢?因为你可以在任何函数中读取和修改它的值,甚至在REPL中也可以随意操作。这种特性让我们在某些情况下可以轻松实现功能,但在其他情况下却可能导致难以追踪的错误。

    🔮 动态作用域

    全局变量的另一个特性是“动态作用域”(Dynamic Scope),它就像是历史的奇妙扭曲,让“恶”变成了“善”。动态作用域允许你在函数中访问全局变量,这种设计理念虽然古怪,却为我们提供了强大的灵活性。

    📜 列表的力量

    在Lisp中,列表是数据的基本形式。你可以通过list操作符来创建列表:

    (list 1 2 3 4)

    这将返回一个包含四个元素的列表 (1 2 3 4)。但Lisp的魅力不仅于此,它的列表可以包含任何类型的对象:

    (list 1 #\a "2" 'symbol-name #'print)

    这将返回一个包含数字、字符、字符串、符号和函数的列表 (1 #\a "2" SYMBOL-NAME #<FUNCTION PRINT>)。Lisp的列表就像是一个百宝箱,里面装满了各种各样的宝藏。

    🚀 列表操作的多样性

    Lisp提供了丰富的操作符,让我们可以对列表进行各种操作。例如,我们可以使用carcdr来获取列表的第一个元素和剩余部分:

    (car *mylist*)   ; => 1
    (cdr *mylist*)   ; => (2 3 4 5)

    这就像是打开一扇窗,透过它我们可以一览无余。更有趣的是,Lisp的列表操作是无副作用的,意味着我们在操作列表时不会改变原始数据。

    🎩 赋值与修改

    在Lisp中,setf是一个强大的赋值操作符,它可以让你随意修改列表中的元素。比如,想把第二个元素改为99:

    (setf (second *mylist*) 99)

    这时,*mylist*将变为 (1 99 3 4 5)。就像魔法师挥动魔杖,瞬间改变了世界。

    ✨ 便捷的列表创建

    如果你觉得使用list创建列表太繁琐,可以通过单引号简化操作:

    '(1 2 symbol-1 symbol-2 "a string")

    这将返回相同的列表,而不需要额外的函数调用。这样一来,编写代码就像是在书写诗篇,优雅而简洁。

    🌈 结论:Lisp的无限可能

    Lisp以其独特的全局变量和强大的列表操作,为我们提供了无尽的编程可能。无论是定义全局变量,还是操作列表,Lisp都展现出其独特的魅力和灵活性。正如武术的每一个动作都蕴含着深刻的哲理,Lisp的每一行代码也都在诉说着编程的美妙。

    在这个充满挑战与机遇的编程世界中,掌握Lisp的技巧就像是获得了无与伦比的力量。让我们继续探索,发现更多编程的乐趣吧!


    面向记忆的学习材料

    任务目标

    快速学习并记住 Common Lisp 中关于全局变量和列表的相关知识

    知识点: Lisp 的命名由来
    题目: Lisp 的名称源自于哪个短语?
    选项:
    A. List Processing
    B. Logic In Simple Programming
    C. Language for Intelligent System Programming
    D. Logical Instruction Set Programming

    正确答案: A
    解析: Lisp 是 “List Processing”(列表处理)的缩写。这个名称反映了 Lisp 语言的核心特性,即它主要用于处理列表数据结构。在 Lisp 中,代码本身也是以列表的形式表示的,这种特性被称为同像性(homoiconicity)。
    速记提示: 记住 “List” 这个关键词,Lisp = List Processing。

    知识点: Lisp 的同像性
    题目: Lisp 语言的哪个特性使得它可以将代码作为数据处理,反之亦然?
    选项:
    A. 动态作用域
    B. 垃圾回收
    C. 同像性
    D. 函数式编程

    正确答案: C
    解析: Lisp 的同像性(homoiconicity)是指代码和数据使用相同的表示形式。这使得 Lisp 可以轻松地将代码作为数据处理,或将数据作为代码执行。这是 Lisp 独特而强大的特性,使其在元编程方面具有显著优势。
    速记提示: 同像性 = 代码即数据,数据即代码。

    知识点: 全局变量的命名约定
    题目: 在 Common Lisp 中,全局变量和参数通常使用什么命名约定?
    选项:
    A. 全部大写
    B. 驼峰命名法
    C. 下划线分隔
    D. 星号包围(earmuffs)

    正确答案: D
    解析: Common Lisp 中,全局变量和参数通常使用星号(*)包围的命名方式,例如 *my-var。这种命名约定被称为 “earmuffs”(耳罩),目的是提醒开发者这是一个具有潜在”邪恶”(全局影响)的变量。虽然不是所有人都同意这个约定,但它在 Common Lisp 社区中被广泛使用。
    速记提示: 全局变量戴”耳罩”(var*)。

    知识点: defvar 的特性
    题目: 使用 defvar 定义全局变量时,如果变量已存在,重新编译 defvar 语句会发生什么?
    选项:
    A. 变量值会被更新
    B. 变量值保持不变
    C. 会抛出错误
    D. 变量会被重新创建

    正确答案: B
    解析: 使用 defvar 定义的全局变量具有特殊性质。如果变量已经存在,重新编译 defvar 语句不会改变变量的现有值。这个特性使得 defvar 适合用于定义那些只需要初始化一次的全局变量。只有在变量首次被引入 Lisp 镜像时,defvar 中的初始值才会生效。
    速记提示: defvar 定义的变量,重编译不改值。

    知识点: defparameter 的特性
    题目: defparameter 与 defvar 在重新编译时的主要区别是什么?
    选项:
    A. defparameter 不允许重新编译
    B. defparameter 每次重新编译都会重新绑定值
    C. defparameter 只能用于定义常量
    D. defparameter 不支持文档字符串

    正确答案: B
    解析: defparameter 和 defvar 的主要区别在于,每次重新编译 defparameter 定义时,变量都会被重新绑定到指定的值。这使得 defparameter 更适合用于定义那些可能需要在开发过程中频繁更改的设置或参数。而 defvar 则只在变量首次定义时设置初始值。
    速记提示: parameter 参数可变,每次编译都更新。

    知识点: defconstant 的用途
    题目: 在 Common Lisp 中,defconstant 的主要用途是什么?
    选项:
    A. 定义可变的全局变量
    B. 定义不可变的常量
    C. 定义局部变量
    D. 定义函数

    正确答案: B
    解析: defconstant 用于定义不可变的全局常量。一旦使用 defconstant 定义了一个常量,尝试修改它的值将会导致错误。这保证了常量的不可变性。然而,在实际应用中,除了给数值命名外,defconstant 的使用相对较少,更常见的做法是使用 defparameter 来定义”常量”。
    速记提示: constant 常量,定义后不可变。

    知识点: 列表的创建
    题目: 在 Common Lisp 中,下列哪个表达式不能创建列表 (1 2 3 4)?
    选项:
    A. (list 1 2 3 4)
    B. ‘(1 2 3 4)
    C. (quote (1 2 3 4))
    D. (cons 1 2 3 4)

    正确答案: D
    解析: 前三个选项都是创建列表 (1 2 3 4) 的有效方式。(list 1 2 3 4) 使用 list 函数创建列表,'(1 2 3 4) 使用引用语法创建列表,(quote (1 2 3 4)) 是 ‘(1 2 3 4) 的完整形式。然而,(cons 1 2 3 4) 是错误的语法,cons 函数只接受两个参数,用于创建点对(dotted pair)或向列表头部添加元素。
    速记提示: cons 只接受两个参数,不能直接创建多元素列表。

    知识点: car 和 first 函数
    题目: 在 Common Lisp 中,car 和 first 函数的区别是什么?
    选项:
    A. car 用于获取列表的第一个元素,first 用于获取第二个元素
    B. car 只能用于点对,first 只能用于列表
    C. car 和 first 完全等价,没有区别
    D. car 是低级操作,first 是高级抽象

    正确答案: C
    解析: 在 Common Lisp 中,car 和 first 函数实际上是完全等价的。它们都用于获取列表的第一个元素。car 源自早期 Lisp 实现中的”Contents of Address Register”(地址寄存器内容),而 first 是一个更具描述性的名称。使用哪个主要是风格选择,first 可能更易读,特别是对新手来说。
    速记提示: car 和 first 同义,都取列表首元素。

    知识点: cdr 和 rest 函数
    题目: 对于列表 (1 2 3 4),调用 (cdr list) 会返回什么?
    选项:
    A. 1
    B. (2 3 4)
    C. (1 2 3)
    D. 4

    正确答案: B
    解析: cdr 函数(等同于 rest 函数)返回原列表除去第一个元素后的剩余部分。对于列表 (1 2 3 4),调用 (cdr list) 会返回 (2 3 4)。cdr 源自”Contents of Decrement Register”(减量寄存器内容),而 rest 是一个更直观的名称,表示”剩余部分”。
    速记提示: cdr/rest 返回去掉首元素后的子列表。

    知识点: nth 函数
    题目: 对于列表 (a b c d e),表达式 (nth 2 list) 会返回什么?
    选项:
    A. b
    B. c
    C. d
    E. e

    正确答案: B
    解析: nth 函数用于获取列表中的第 n 个元素,其中 n 从 0 开始计数。因此,对于列表 (a b c d e),(nth 2 list) 会返回第三个元素,即 c。要注意的是,nth 的计数是从 0 开始的,这与一些其他编程语言中的索引习惯一致。
    速记提示: nth 从 0 开始数,2 对应第三个元素。

    知识点: append 函数
    题目: 表达式 (append ‘(1 2) ‘(3 4)) 的结果是什么?
    选项:
    A. (1 2 3 4)
    B. ((1 2) (3 4))
    C. (1 2 (3 4))
    D. (3 4 1 2)

    正确答案: A
    解析: append 函数用于连接两个或多个列表。它会创建一个新的列表,包含所有输入列表的元素,按照输入的顺序依次排列。因此,(append ‘(1 2) ‘(3 4)) 会返回 (1 2 3 4)。需要注意的是,append 不会修改原有的列表,而是创建一个新的列表。
    速记提示: append 追加,按序连接多个列表。

    知识点: reverse 函数
    题目: 对列表 (1 2 3 4) 调用 reverse 函数后,原列表会发生什么变化?
    选项:
    A. 列表变为 (4 3 2 1)
    B. 列表保持不变
    C. 列表变为 ((1 2 3 4))
    D. 列表变为 (1)

    正确答案: B
    解析: reverse 函数会返回一个新的、元素顺序相反的列表,但不会修改原始列表。这是因为 Common Lisp 中的大多数列表操作函数都不会产生副作用(side effects)。因此,对列表 (1 2 3 4) 调用 reverse 后,原列表保持不变,而函数返回一个新的列表 (4 3 2 1)。
    速记提示: reverse 返回新列表,原列表不变。

    知识点: push 函数
    题目: 如果 mylist 的值为 (2 3 4),执行 (push 1 mylist) 后,mylist 的新值是什么?
    选项:
    A. (2 3 4 1)
    B. (1 2 3 4)
    C. ((1) 2 3 4)
    D. (1 (2 3 4))

    正确答案: B
    解析: push 函数是少数会修改原列表的函数之一。它将一个新元素添加到列表的开头,并更新列表变量。因此,执行 (push 1 mylist) 后,mylist 的新值为 (1 2 3 4)。push 是一个宏,它实际上是使用 cons 和 setf 的组合来实现的。
    速记提示: push 推到列表头部,改变原列表。

    知识点: pop 函数
    题目: 如果 mylist 的值为 (1 2 3 4),执行 (pop mylist) 会返回什么,mylist 的新值又是什么?
    选项:
    A. 返回 1,mylist 变为 (2 3 4)
    B. 返回 (1),mylist 变为 (2 3 4)
    C. 返回 4,mylist 变为 (1 2 3)
    D. 返回 (1 2 3 4),mylist 变为 nil

    正确答案: A
    解析: pop 函数会移除并返回列表的第一个元素,同时修改原列表。因此,执行 (pop mylist) 会返回 1,并且 mylist 的新值变为 (2 3 4)。pop 也是一个宏,它结合了 car 和 setf 的功能。
    速记提示: pop 弹出并返回首元素,改变原列表。

    知识点: 引用语法
    题目: 在 Common Lisp 中,'(1 2 symbol) 等价于以下哪个表达式?
    选项:
    A. (list 1 2 ‘symbol)
    B. (quote (1 2 symbol))
    C. (cons 1 (cons 2 (cons ‘symbol nil)))
    D. (1 2 symbol)

    正确答案: B
    解析: 在 Common Lisp 中,’是 quote 的简写形式。因此,'(1 2 symbol) 完全等价于 (quote (1 2 symbol))。这种语法用于创建字面量列表,其中的符号不会被求值。这与使用 list 函数创建列表不同,因为 list 函数会对其参数进行求值。
    速记提示: 单引号’是 quote 的简写,用于创建字面量列表。

    知识点: setf 的通用性
    题目: 以下哪个不是 setf 的有效用法?
    选项:
    A. (setf (car list) 10)
    B. (setf (aref array 0) 20)
    C. (setf (length list) 5)
    D. (setf (gethash key hash-table) value)

    正确答案: C
    解析: setf 是一个通用的赋值操作符,可以用于多种数据结构。选项 A 用于设置列表的第一个元素,B 用于设置数组的元素,D 用于设置哈希表的值,这些都是有效的用法。然而,C 是无效的,因为 length 是一个只读函数,不能直接设置列表的长度。
    速记提示: setf 通用赋值,但不能改变只读属性如 length。

    知识点: 列表元素的修改
    题目: 如果 mylist 的值为 (1 2 3 4 5),执行 (setf (second mylist) 99) 后,mylist 的新值是什么?
    选项:
    A. (1 99 3 4 5)
    B. (99 2 3 4 5)
    C. (1 2 99 4 5)
    D. (1 2 3 4 99)

    正确答案: A
    解析: setf 可以与访问函数如 second 结合使用,来修改列表的特定位置的元素。second 函数返回列表的第二个元素,因此 (setf (second mylist) 99) 会将列表的第二个元素改为 99。执行后,mylist 的新值为 (1 99 3 4 5)。
    速记提示: (setf (second list) value) 修改第二个元素。

    知识点: 列表的替换
    题目: 执行 (setf mylist ‘(1 2 3)) 后,mylist 的值会是什么?
    选项:
    A. (1 2 3)
    B. ((1 2 3))
    C. (list 1 2 3)
    D. ‘(1 2 3)

    正确答案: A
    解析: 这个 setf 表达式直接将 mylist 变量的值设置为新的列表 (1 2 3)。'(1 2 3) 创建了一个字面量列表,然后 setf 将这个新列表赋值给 mylist。执行后,mylist 的值就是 (1 2 3)。这展示了如何使用 setf 完全替换一个列表变量的内容。
    速记提示: setf 可直接替换整个列表。

    总结

    本学习材料涵盖了 Common Lisp 中关于全局变量和列表操作的重要概念:

    1. Lisp 的命名由来和同像性特征。
    2. 全局变量的定义方式(defvar, defparameter, defconstant)及其特性。
    3. 列表的创建方法,包括 list 函数和引用语法。
    4. 常用的列表操作函数,如 car/first, cdr/rest, nth, append, reverse 等。
    5. 修改列表的函数,如 push, pop, setf 等。
    6. 引用语法(‘和 quote)的使用。
    7. setf 的通用性和在列表操作中的应用。

    这些知识点构成了 Common Lisp 中处理全局变量和列表的基础。掌握这些概念将有助于理解和编写 Lisp 程序,特别是在处理数据结构和进行元编程时。建议读者多加练习这些操作,以熟练掌握 Lisp 的列表处理能力。

    参考文献

    http://l1sp.org

    Common Lisp – The Tutorial Part 6.pdf

    https://github.com/rabbibotton/clog/blob/main/LEARN.md


  • 🌊 在“包”与“系统”的海洋中航行:Common Lisp 的探险

    在这个信息爆炸的时代,编程语言如雨后春笋般层出不穷,但在这片繁茂的森林中,Common Lisp犹如一颗璀璨的明珠,闪耀着智慧的光芒。它的稳定性和灵活性,使得它成为了一种在真实世界中编写软件的强大工具。今天,我们将深入探讨Common Lisp中的“包”和“系统”,并通过一些趣味的比喻,让这个复杂的概念变得通俗易懂。

    📦 包的构造:符号的家园

    在Common Lisp中,包(Package)就像是一个个小社区,里面住着许多符号(Symbols)。这些符号就像社区中的居民,它们各自有着不同的角色和功能。想象一下,如果没有组织,居民们会变得混乱不堪,甚至可能会互相争吵。包正是在这样的背景下应运而生,它为符号提供了一个有序的居住环境。

    🏠 创建你的第一个包

    创建一个包的过程就像是为新社区立法。我们可以通过以下代码来创建一个简单的包:

    (defpackage :hello-package
      (:nicknames :hello-pkg)
      (:use :cl :clog)
      (:export :hello-world))

    在这个包中,我们定义了一个社区的名称(hello-package),设置了一个昵称(hello-pkg),并指定了它能使用的其他包(:cl 和 :clog)。最重要的是,我们通过:exporthello-world这个符号暴露给外界,就像是把社区的门打开,让外人也能进来。

    🧭 进入包的世界

    当你想要进入某个包时,可以使用IN-PACKAGE命令,就像是走进一个社区的大门。比如,我们可以通过以下命令进入hello-package

    (in-package :hello-package)

    此时,你就可以在这个包内创建和管理符号,而不必担心与其他包的符号发生冲突。

    🔗 ASDF与QuickLisp:构建系统的基石

    要想把你的包变成一个完整的系统,就需要用到ASDF和QuickLisp这两个强大的工具。它们就像是建筑工程师和材料供应商,帮助你将各种组件组合成一个完整的建筑。

    🏗️ 定义系统

    定义一个系统的过程就像是绘制一张建筑蓝图。以下是一个简单的系统定义示例:

    (asdf:defsystem #:hello-sys
      :description "Common Lisp - The Tutorial Part 3"
      :author "david@botton.com"
      :license "BSD"
      :version "0.0.0"
      :serial t
      :depends-on (#:clog)
      :components ((:file "hello")))

    在这段代码中,我们描述了系统的基本信息,包括名称、描述、作者以及依赖的包。最后,通过:components指定了系统中包含的文件。

    📦 快速加载系统

    一旦系统定义完成,我们可以使用QuickLisp来加载这个系统,这就像是按下了建筑的启动按钮:

    (ql:quickload :hello-sys)

    这时,所有相关的组件和依赖都会被自动加载,准备就绪。

    🎉 运行你的系统

    一旦系统被加载,我们就可以开始测试自己的代码了。调用hello-world函数,就像是打开了一扇通往新世界的大门:

    (hello-pkg:hello-world)

    如果一切顺利,你的浏览器将会打开,展示出“Hello World”的标语,仿佛在向你致以热烈的欢迎。

    🤔 总结与反思

    通过今天的探险,我们了解了Common Lisp中的包与系统的基本概念与使用方法。包为我们提供了组织符号的结构,而ASDF和QuickLisp则帮助我们将这些符号组合成一个完整的系统。正如《海底两万里》中所描绘的那样,编程的旅程充满了未知的挑战与惊喜,而Common Lisp便是我们探索这片海洋的强大潜艇。

    当然,编程之路并非一帆风顺,理解这些概念需要时间与实践。希望大家能在这个过程中,享受到编程带来的乐趣与成就感。

    📚 参考文献

    1. Common Lisp – The Tutorial Part 3, David Botton.
    2. The Evolution of Lisp, Paul Graham.
    3. Practical Common Lisp, Peter Seibel.
    4. ANSI Common Lisp, Graham, Paul.
    5. Lisp In Small Pieces, Christian Queinnec.

    在 Common Lisp 中,#:foo 表示一个未归属到任何包的符号,而不是全局符号。以下是对 #:foo 和全局变量的详细区别和解释:

    1. #:foo 的性质

    • 未归属符号#:foo 是一个独立的符号,它不是任何包的一部分。这意味着它不会与全局符号或其他包中的符号冲突。
    • 临时性:通常用于临时用途或内部数据结构,适合在函数内部或局部上下文中使用。

    2. 全局变量的性质

    • 全局变量:全局变量是在程序的全局上下文中定义的符号,通常是在特定包中定义且可以被其他包访问。
    • 可见性:全局变量可以在多个包中被引用,只要适当指定包名(例如 package-name:variable-name),并且它们的值在整个程序的生命周期中保持有效。

    3. 关键区别

    • 命名空间
      • #:foo 是未归属的,意味着它既不在全局符号表中,也不在任何包中。
      • 全局变量是包的一部分,通常具有明确的包名和作用域。
    • 冲突
      • #:foo 不会与其他符号发生冲突,因为它是独立的。
      • 全局变量可能与其他包中的同名变量发生冲突,除非在使用时明确指定包。
    • 使用场景
      • #:foo 适合临时用途,如在宏或函数内部使用。
      • 全局变量用于需要在多个模块或包之间共享的状态或数据。

    示例

    ;; 定义全局变量
    (defparameter *my-global* 42)
    
    ;; 使用 #:foo
    (let ((#:foo 100))  ; #:foo 是一个临时符号
      (print #:foo))    ; 输出 #:foo,没有冲突
    
    ;; 使用全局变量
    (print *my-global*)  ; 输出 42

    总结

    • #:foo 是一个未归属的符号,通常用于临时用途,不会与其他符号发生冲突。
    • 全局变量是在程序的上下文中定义的符号,可以被多个包访问,可能会与其他包中的同名符号冲突。
    • 选择使用 #:foo 还是全局变量,取决于你的具体需求和上下文。

    在 Common Lisp 中,#:#' 是两种不同的读宏(reader macro),它们用于不同的目的。以下是它们的比较:

    1. #' 的含义

    • 功能#' 用于获取一个符号所代表的函数的引用。它是 function 的缩写,常用于指定函数对象。
    • 用法
    (defun my-function ()
      (print "Hello, World!"))
    
    (let ((f #'my-function))  ; 获取 my-function 的引用
      (funcall f))            ; 调用 my-function
    • 用途:主要用于高阶函数、回调和动态调用函数。

    2. #: 的含义

    • 功能#: 用于创建一个未归属的符号,通常用于标识符的命名,特别是在避免符号冲突时。它创建的符号不会与当前包或全局符号表中的符号发生冲突。
    • 用法
    (let ((s #:foo))  ; 创建一个未归属的符号
      (print s))      ; 输出 #:foo
    • 用途:适用于临时用途、宏展开或内部数据结构,常用于需要确保符号唯一性的场景。

    3. 关键区别

    特性#'#:
    目的获取函数的引用创建未归属的符号
    使用场景高阶函数、回调避免符号冲突、临时用途
    符号类型返回一个函数对象返回一个未归属的符号
    冲突可能与全局或其他包中的符号冲突不会与任何其他符号冲突

    总结

    • #' 用于获取函数的引用,适合高阶函数和动态调用。
    • #: 用于创建未归属的符号,适合临时用途和避免符号冲突。

    领略共生:Lisp 中的函数之美 🌱

    在编程的世界里,有一种语言如同一位优雅的舞者,轻盈而灵动,仿佛在与代码共舞——这就是 Lisp。正如电影《公爵与我》中所说:“你总是使用那个词。我不认为它意味着你认为的那样。”今天,我们就来聊聊 Lisp 中的函数,揭开其神秘的面纱。

    🚀 函数的速度与灵活性

    在编程的宇宙中,Lisp 以其不可思议的速度和灵活性著称。想象一下,你在 REPL(Read-Eval-Print Loop)中如同一位魔法师,随意施展咒语,看到代码瞬间生效。为了让这些魔法长久地存在,我们需要将它们植根于系统中,这就需要定义命名函数。函数不仅是执行操作的工具,更是数据的一部分,可以作为参数传递或从另一个函数返回。Lisp,作为函数式编程的祖师爷,赋予了函数以新的生命。

    🏗️ 命名函数的构建

    让我们来构建一个简单的系统,名为 hello-sys。首先,我们需要加载这个系统:

    (ql:quickload :hello-sys)

    在我们的 hello-package 中,定义了两个命名函数,一个是导出的 hello-world,另一个是私有的 hello-private

    (defpackage :hello-package
      (:nicknames :hello-pkg)
      (:use :cl :clog)
      (:export :hello-world))
    
    (in-package :hello-package)
    
    (defun hello-private (body)
      "Create a div on new pages containing - hello world"
      (create-div body :content "hello world"))
    
    (defun hello-world ()
      "Initialize CLOG and open a browser"
      (initialize 'hello-private)
      (open-browser))

    在这里,defun 是定义命名函数的魔法咒语。每个函数都可以有参数,甚至可以没有参数。想象一下,调用 hello-world 就像是打开一扇窗,让阳光洒进来,而 my-func 则是一个简单的加法函数:

    (defun my-func (parameter1 parameter2)
      "A DOC string used for documenting a named function"
      (+ parameter1 parameter2))

    ⚙️ 可选与命名参数

    函数的灵活性不仅体现在可以无参数或多参数上,还可以使用可选参数和命名参数。下面的例子展示了如何使用可选参数:

    (defun my-func2 (parameter1 &optional parameter2)
      "return the sum of parameters unless only parameter1 provided."
      (if parameter2
          (+ parameter1 parameter2)
          parameter1))

    调用 my-func2 的时候,你可以选择只传入一个参数,或者两个参数。就像在餐厅里,你可以选择点一份主菜,也可以加一个配菜。

    🔍 匿名函数的魅力

    不仅有命名函数,Lisp 还拥有一种神秘的存在——匿名函数。通过 lambda 表达式,我们可以创建没有名字的函数,就像在舞台上闪烁的流星,转瞬即逝,但却璀璨夺目:

    (lambda (lambda-list) body)

    hello-private 函数中,我们使用了匿名函数来处理颜色的变化:

    (defun hello-private (body)
      "Create a div on new pages containing - hello world
      That when clicked changes color."
      (set-on-click (create-div body :content "hello world")
                    (lambda (obj)
                      (setf (color obj) (rgb (random 255)
                                             (random 255)
                                             (random 255))))))

    这里,set-on-click 生成的 lambda 表达式就像是一位调皮的色彩师,随意调配出五彩斑斓的颜色。

    🎉 结论:函数的魔力

    函数在 Lisp 中不仅仅是操作数据的工具,它们可以有副作用,比如打印输出或生成图形,甚至可以作为数据本身。正是这些特性,让 Lisp 成为一种独特的函数式编程语言。

    📚 总结

    从上述内容中,我们可以提炼出以下几点:

    1. Lisp 使用“LISt Processing”。
    2. 列表的第一个元素是操作符,其余元素是参数。
    3. Lisp 拥有不同的开发周期和模型,程序是逐步生长的。
    4. 在 Lisp 程序中,包是结构的顶层,组织符号而非文件。
    5. 完全合格的符号以 package-name:symbol-name 编码。
    6. 符号可以命名函数、变量等。
    7. 符号也可以作为数据类型使用,前面加上撇号。
    8. 我们通过 IN-PACKAGE 在包之间导航。
    9. 使用 DEFPACKAGE 定义包。
    10. 系统通过一个目录定义,该目录与 .asd 配置文件同名。

    Lisp 的函数就如同万花筒中的光影,变化多端,令人目不暇接。它们不仅让我们能够高效地处理数据,更让编程的过程充满乐趣与创造力。


    在 Common Lisp 中,&optional&key 是用来定义函数参数的两种不同方式,它们各有千秋,适合不同的使用场景。让我们来逐一探讨这两者的区别,像是在一场精彩的辩论中,看看谁更胜一筹。

    🎭 可选参数:&optional

    &optional 用于定义可选参数,这意味着你可以选择性地提供这些参数。如果你不提供可选参数,Lisp 会将它们的值设置为 nil。让我们通过一个示例来看看:

    (defun my-func2 (parameter1 &optional parameter2)
      "Return the sum of parameters unless only parameter1 is provided."
      (if parameter2
          (+ parameter1 parameter2)
          parameter1))

    在这个例子中,parameter1 是必填参数,而 parameter2 是可选的。如果调用时只提供 parameter1,那么 parameter2 就会是 nil,此时函数返回 parameter1 的值。调用方式如下:

    (my-func2 5)      ; 返回 5
    (my-func2 5 3)    ; 返回 8

    🌈 命名参数:&key

    &optional 不同,&key 用于定义命名参数。命名参数允许你在调用函数时通过参数名来指定参数,而不是仅仅依靠它们的位置。这使得函数的调用更加灵活和可读。来看一个例子:

    (defun my-func3 (parameter1 &key parameter2)
      "Return the sum of parameters unless only parameter1 is provided."
      (if parameter2
          (+ parameter1 parameter2)
          parameter1))

    在这种情况下,你可以按如下方式调用函数:

    (my-func3 5 :parameter2 3)  ; 返回 8
    (my-func3 5)                 ; 返回 5

    通过这种方式,你可以更清楚地表明你所提供的参数是什么,尤其是当函数有多个可选参数时,这种方式让代码更加易读。

    🏆 总结:选择你的战斗方式

    • &optional:用于定义可选参数,适合简单的函数调用,特别是参数较少时。
    • &key:用于定义命名参数,适合参数较多、需要提高可读性和灵活性的情况。

    选择哪种方式,完全取决于你的具体需求。如果你想让函数调用更清晰,尤其是当函数参数众多时,使用 &key 是个不错的选择;而如果只是简单的可选参数,&optional 则更为直观。

    这就像在一个派对上,你可以选择直接邀请所有人(&optional),或者通过特定的邀请函来让每个人知道他们的角色(&key)。选择权在你!🎉


    在 Common Lisp 中,冒号 : 有着特殊的含义,尤其是在使用命名参数时。让我们一起深入了解一下这个小符号的魔力吧!🔍

    🌟 冒号的含义

    在函数调用中,冒号用于表示命名参数(&key 参数)。当你在调用一个使用了 &key 的函数时,冒号后面跟的是参数的名称,这样你可以明确指定要传递的参数。例如:

    (my-func3 5 :parameter2 3)

    在这个例子中:

    • 5 是位置参数 parameter1 的值。
    • :parameter2 是命名参数,后面的 3 是传递给 parameter2 的值。

    命名参数的优势

    使用冒号来指明参数名有几个优点:

    1. 可读性:通过明确指定参数名,代码的意图变得更加清晰。例如,(my-func3 5 :parameter2 3)(my-func3 5 3) 更容易理解,因为你可以一眼看出 3 是赋给 parameter2 的值。
    2. 灵活性:当函数有多个可选参数时,使用命名参数可以让你在调用时灵活选择,只传递你关心的参数。例如,如果你只想传递 parameter2,可以这样调用:
       (my-func3 5 :parameter2 3)  ; 传递 parameter1 和 parameter2
       (my-func3 5)                 ; 只传递 parameter1,parameter2 为 nil
    1. 顺序无关:使用命名参数时,参数的顺序不再重要。你可以随意组合参数,只需确保使用了正确的参数名。

    🎉 小结

    在 Common Lisp 中,冒号 : 用于指示命名参数,这使得函数调用更加灵活、可读和清晰。它让代码更具表达力,就像为你的函数调用加上了一层精美的装饰,让它不再单调乏味!

    所以,下次在使用命名参数时,记得给你的代码加点颜色,充分利用这个小小的冒号吧!✨


    在 Common Lisp 中,你可以使用 Unicode 字符来定义 lambda 表达式,甚至可以用 Unicode 字符作为函数名。这就像为你的代码穿上了华丽的衣服,让它更具特色和趣味性。实际上,Lisp 是相当灵活的,允许许多非传统的字符来丰富你的代码风格。

    🌟 使用 Unicode 字符定义 Lambda

    首先,让我们看看如何使用 Unicode 字符来定义一个简单的 lambda 函数。假设我们想用希腊字母 λ(Unicode: U+03BB)来表示我们的 lambda 函数:

    (defun λ (x)
      (* x x))

    在这个例子中,我们定义了一个名为 λ 的函数,它接受一个参数 x,并返回 x 的平方。现在,我们可以像调用任何其他函数一样调用这个函数:

    (λ 3)  ; 返回 9

    🌈 多样化字符的乐趣

    Lisp 的这种灵活性不仅限于 lambda,你可以使用其他 Unicode 字符来定义变量、参数和函数名。这就像是在编程中加入了一点艺术的色彩,让代码不仅仅是逻辑的堆砌,更是一种表达。

    例如,你可以使用中文字符来定义函数:

    (defun 加法 (a b)
      (+ a b))

    调用这个函数时,你可以写:

    (加法 2 3)  ; 返回 5

    ⚠️ 注意事项

    尽管使用 Unicode 字符可以让你的代码更具个性,但也要注意以下几点:

    1. 可读性:虽然对你来说可能很有趣,但其他人可能不习惯看到 Unicode 字符,可能会影响代码的可读性。
    2. 兼容性:确保你的开发环境和版本控制系统支持 Unicode 字符。如果不支持,可能会导致意外的错误。
    3. 调试:在调试时,使用非标准字符可能会让错误信息变得更加复杂,尤其是在大型项目中。

    🎉 结论

    总之,使用 Unicode 字符来定义 lambda 和其他函数在 Common Lisp 中是完全可行的。这种灵活性不仅让编程变得更有趣,还为你提供了更多的表达方式。只要确保在可读性和兼容性之间找到平衡,你就可以大胆地让你的代码焕发出独特的光彩!✨


    在 Common Lisp 中,确实可以使用 Unicode 字符 λ(U+03BB)来替换 lambda,但这并不是一个直接的替换。让我们深入探讨这一点。

    🌟 使用 λ 的潜力

    在 Lisp 中,lambda 是一个特殊形式,用于定义匿名函数。虽然你可以用 λ 来作为函数名,但如果你想直接使用 λ 来定义一个匿名函数,实际上是不可行的。Lisp 仍然要求使用 lambda 关键字来明确表示你正在定义一个匿名函数。

    例如,以下是定义一个匿名函数的传统方式:

    (lambda (x) (* x x))

    如果你想用 λ 来表示这个函数,可以将其用作函数名,但仍需使用 lambda 关键字:

    (defun λ (x) (* x x))

    因此,调用这个函数时,你可以这样做:

    (λ 3)  ; 返回 9

    🔍 λ 作为函数名与 lambda 的区别

    在 Lisp 中,lambda 关键字是语言的核心部分,用于创建匿名函数。而 λ 作为一个普通的符号,只能用于命名函数。换句话说,虽然你可以用 λ 来命名函数,但不能用它来替代 lambda 关键字本身。

    🎉 小结

    • 直接替换:不能用 λ 替代 lambda 关键字来定义匿名函数。
    • 作为函数名:你可以使用 λ 作为函数名,定义并调用它,就像使用任何其他标准字符一样。

    所以,尽管 λ 不能直接替代 lambda,你仍然可以利用它为你的代码增添一抹独特的风采。编程就是这样一门艺术,尝试不同的符号和表达方式,可能会让你的代码更加生动有趣!🎨✨


  • 🚀 探索 Common Lisp 的奇妙世界

    在编程的宇宙中,有一种语言如同一位伟大的魔法师,掌控着各种神秘的力量,那便是 Common Lisp!今天,我们将踏上这段奇妙的旅程,深入探讨这门语言的核心概念,尤其是它的符号和开发流程。准备好迎接挑战了吗?别担心,我们会像打拳击一样,耐心而又坚定地前行。

    🌱 初识 Common Lisp

    Common Lisp 并不是你所熟悉的编程语言,像是用木刀打靶的初学者,它有自己独特的开发循环和思维方式。你可能习惯于编辑 -> 编译 -> 执行的传统开发流程,但在 Lisp 中,事情大为不同。

    想象一下,Emacs 是你灵活的拳击手套,而 SLIME 就是那根连结你和 Lisp 图像的“脐带”。通过它,你可以实时与 Lisp 对话,像是和一位训练有素的教练进行心灵的交流。你可以在 REPL(读取—求值—打印循环)中输入命令,立刻得到反馈,仿佛在进行一场无声的对话。

    CL-USER> (print "hello")
    "hello"

    就这样,你的第一个命令便在 REPL 中成功执行,返回了 “hello”。这不是简单的输出,而是你与 Lisp 之间一场精彩的互动,仿佛在为你的编程生涯揭开序幕。

    📂 文件与包的奇妙关系

    在 Common Lisp 中,文件并不如其他语言那样提供程序的结构。相反,文件中的每一行代码都像是你在 REPL 中输入的内容,直接注入到 Lisp 图像中。想象一下,你的一切代码都如同一锅浓汤,随时可以舀出,随时可以添加新料。

    那么,如何管理这些代码呢?这就要提到“包”的概念了。包就像是你在厨房中为不同食材准备的碗,每个碗里都是独特的配方。Common Lisp 中的符号就通过包进行组织。例如,common-lisp:print 这个符号指向了打印函数,而在默认的 CL-USER 包中,你可以直接使用 print 来调用它。

    (load "~/common-lisp/hello.lisp")

    当你加载一个文件时,你不仅仅是将代码加入到 Lisp 图像中,而是将其融入到整个程序的结构中。每个包都是一片田野,而符号则是那一颗颗播种的种子,未来会发芽成长,结出丰硕的果实。

    🧩 符号的魔力

    现在,让我们来聊聊符号,这个在 Common Lisp 中具有重要地位的概念。符号不仅仅是代码中的名字,它们是与程序中各个元素的连接点,几乎可以说是语言的灵魂。

    你可以在 REPL 中直接创建符号,或者在文件中定义它们。使用 defun 定义函数,使用 defvar 定义变量,这些都是符号的运用。看一下这个例子:

    (defun main ()
      (print "hello"))
    
    (defvar *cool* 123)
    (setf *cool* nil)

    在这个代码片段中,main*cool* 都是符号。main 是一个函数名,而 *cool* 则是一个全局动态变量。更有趣的是,Lisp 允许你将函数本身赋值给变量,这让代码变得灵活而富有表现力。

    🎨 符号的使用与数据类型

    符号在 Lisp 中不仅仅是用于命名的工具,它们本身也可以作为数据类型使用。通过在符号前加上一个撇号(’),你可以将它们作为类型处理。例如:

    (setf *cool* 'a-symbol)

    这里的 a-symbol 不再是一个字符串,而是一个符号。想象一下,你可以将它用作图形绘制的参数,传递不同的符号来表现不同的情感——当你传递 'love 时,它画出花朵,而传递 'hate 时,它则画出骷髅。这种灵活性为编程增添了不少乐趣。

    🏁 总结与展望

    今天我们探索了 Common Lisp 的几个重要概念:REPL 的使用、文件与包的关系、符号的定义与应用。虽然我们走得慢而稳,但这正是通向编程深处的必要之路。理解这些基础概念,将为你后续的编程旅程打下坚实的基础。

    在接下来的部分中,我们将加速前进,深入探讨 Lisp 的更多奇妙之处。准备好迎接更复杂的挑战了吗?让我们一起继续探索这个充满可能性的编程世界吧!

    📚 参考文献

    1. Common Lisp: The Language, 2nd Edition
    2. Common Lisp – The Tutorial Part 1
    3. https://github.com/rabbibotton/clog/blob/main/LEARN.md
    4. https://lisp-lang.org/learn/getting-started/
    5. https://gigamonkeys.com/book/

    保持好奇,勇敢探索!


    functionsymbol-function 比较

    functionsymbol-function 都可以用来获取函数的引用,但它们之间有一些关键的区别和适用场景。以下是它们的比较:

    1. 语法和用法

    • function:
      • 用法:(function symbol)
      • 适用于已定义的函数。
      • 直接获取函数的引用。
      (defun my-function ()
        (print "Hello!"))
    
      (setf my-var (function my-function))
    • symbol-function:
      • 用法:(symbol-function 'symbol)
      • 适用于任何符号,无论它是否已经被定义为函数。
      • 可以检查符号是否有对应的函数,并获取其引用。
      (setf my-var (symbol-function 'my-function))

    2. 错误处理

    • function:
      • 如果符号未定义为函数,将导致错误。
    • symbol-function:
      • 如果符号未定义为函数,返回 nil,不会产生错误。

    3. 适用场景

    • function:
      • 更常用于直接调用和传递已知函数的引用,语法更简洁。
    • symbol-function:
      • 更适用于动态场景,比如需要检查符号是否已定义为函数,或在需要处理未定义函数的情况下使用。

    4. 赋值

    • symbol-function 允许你重新定义一个函数的实现:
      (setf (symbol-function 'my-function) (lambda () (print "New implementation!")))
    • function 不能直接用于赋值。

    总结

    • 使用 function 时,语法简洁,适合已知函数的引用。
    • 使用 symbol-function 时,提供了更大的灵活性,允许对未定义符号的检查和赋值。

    在 Lisp 中,#' 是用来获取符号所代表的函数的标准方式。如果你不想使用 #',还有其他方法,但这取决于你想要实现的具体功能。

    1. 使用 function

    你可以用 function 关键字替代 #',效果相同:

    (setf *cool* (function main))

    2. 使用 symbol-function

    如果你希望直接从符号中获取函数对象,可以使用 symbol-function

    (setf *cool* (symbol-function 'main))

    3. 不使用函数名

    如果你有一个匿名函数(lambda 表达式),则可以直接将它赋值给变量:

    (setf *cool* (lambda () (print "Hello, World!")))

    总结

    虽然有其他方法可以引用函数,但 #' 是最常用和推荐的方式,因为它简洁且易于理解。使用 functionsymbol-function 也是有效的,但可能在可读性上稍逊一筹。


    在 Common Lisp 中,let* 的语法要求使用括号来定义变量和它们的初始值。以下是对这个语法结构的详细解释:

    1. let* 的基本结构

    let* 的基本结构是:

    (let* ((var1 value1)
           (var2 value2)
           ...)
      body)
    • 外层括号:表示 let* 表达式的整体。
    • 内层括号:每对内层括号表示一个变量及其对应的初始值。

    2. 变量的定义

    let* 中,每个变量都被放在一个括号对中,以便于定义和初始化:

    (let* ((s "This is a string
    that spans multiple lines."))
      ...

    这里的 (s "This is a string...") 是一个内层括号,表示变量 s 的定义,"This is a string..." 是它的初始值。

    3. 支持依赖关系

    使用 let* 而不是 let 的原因之一是,let* 支持变量之间的依赖关系。在 let* 中,后面定义的变量可以使用前面变量的值。例如:

    (let* ((x 10)
           (y (+ x 5)))  ; y 可以使用 x 的值
      (print y))      ; 输出 15

    在这个例子中,y 的值依赖于 x 的值。

    结论

    因此,使用两层括号是为了在 let* 中清晰地定义每个变量及其初始值。这是 Common Lisp 语法的一部分,使得代码结构更加明确。


  • 🚀 自然语言与编程的奇妙旅程:探索 Common Lisp 的美与力量

    引言 🌌

    在这个信息爆炸的时代,编程语言如同万花筒般层出不穷,每一种语言都有它独特的魅力。然而,今天我们要聚焦的却是一种被许多人视为“丑陋”的语言——Common Lisp。可别被这个外表所迷惑,正如保罗·格雷厄姆所言:“如果 CL 很丑,为什么我还要用它和写关于它的文章呢?因为 Lisp 实在是太强大了,即便是一个丑陋的 Lisp,也比其他语言更可取。”从某种意义上来说,Lisp 就像是编程语言中的“丑小鸭”,在它内敛的外表下,蕴藏着无穷的力量。

    📜 旅程的开始

    今天,我们将一起踏上探索 Common Lisp 的旅程。我,一个写作水平堪忧的程序员,以及你,一个渴望学习 Common Lisp、CLOG 或者神秘的卡巴拉软件设计的好奇者。我们的目标是迅速让你掌握足够的 Common Lisp,以便你能用这种充满括号的丑陋美丽语言,编写出比任何其他语言更强大、更迅速的软件。

    规则 1:忽略括号,关注缩进 🔍

    在 Lisp 的世界里,S 表达式(S-expressions)是其核心。一个括号后跟一个操作符和参数,最后以另一个括号结束。例如,(+ 1 2) 的结果是 3。这种表达方式使得 Lisp 的结构清晰,但也意味着会有大量的括号出现。然而,别担心,人的大脑有着奇妙的解析能力,只需关注缩进,括号就不再是问题。例如:

    (tagbody
      (print "Hello")
      (go 10))

    一旦你开始关注缩进,Lisp 将会变得和其他语言一样易于理解。

    📈 Lisp 的力量:同构性与多范式

    Lisp 语言的简单语法和括号的使用,使其展现出强大的同构性(homoiconicity)。这意味着代码可以以数据的形式进行处理,构建出更灵活的程序。Lisp 不仅是一种函数式语言,还支持面向对象、过程式等多种编程范式。简而言之,你可以在 Lisp 中随心所欲地编写代码,只要它清晰易读,符合你的需求。

    规则 2:清空思维,使用力量 🧘

    Lisp 的多范式特性使得它可以适应各种编程风格。无论是函数式编程、面向对象编程,还是过程式编程,Lisp 都能游刃有余。重要的是,编写代码的时候要保持清晰和简洁,这样才能在复杂的项目中游刃有余。

    规则 3:稳定性的重要性 🔒

    Lisp 自 1958 年被指定以来,其稳定性一直是其成功的关键因素之一。Common Lisp 于 1994 年成为 ANSI 标准,并自此保持稳定。这种稳定性使得开发者能够在坚实的基础上构建应用,正如一座雄伟的建筑,必须有一个坚固的地基。

    规则 4:社区的力量 💬

    加入 Lisp 社区,你会发现一群乐于分享经验和知识的人。他们的交流虽然直接,但却是确保大家在同一条船上的有效沟通方式。在这里,你可以得到无尽的支持和灵感。

    规则 5:工具的重要性 🛠️

    编程语言的成功往往取决于其工具的丰富性与开源性。Common Lisp 拥有众多开源编译器和强大的工具链,这为开发者提供了极大的便利。记住,工具是你最好的朋友,善待它们,你的编程之旅将更加顺畅。

    🌈 我们的旅程将带你去哪里

    作为一名“战壕程序员”,我不是为了为你编写标准或手册,但我希望能激发你用 CLOG 编写 Common Lisp 代码的热情。接下来,我们将一起探索:

    • 编写商业/IT 应用程序所需的最小操作符集。
    • 学习基本的 Lisp 习惯用法。
    • 引导你深入探索 Lisp 和软件开发的深度。
    • 从一开始就了解并使用并行计算——是的,这在今天是必须的!
    • 当然,成为一个 CLOGer!

    结论 🌟

    在这条编程的旅程中,你不仅会学习到 Common Lisp 的基本知识,还会领悟到编程的真正艺术。Lisp 不仅仅是一种语言,它是一种思维方式,一种创造的力量。让我们一起在这个奇妙的世界中,探索无尽的可能性,编写出更美好的代码,创造出更精彩的未来。

    参考文献 📚

    1. Graham, P. (2002). Common Lisp: The Power of the Parentheses.
    2. Ichbiah, J. (1980). Ada Programming Language.
    3. McCarthy, J. (1958). Lisp Programming Language.
    4. CLOG Documentation. Retrieved from https://github.com/rabbibotton/clog.
    5. The Lisp Community. Retrieved from https://www.cliki.net/.

    希望这段旅程能激励你深入探索 Common Lisp 的广阔天地,并在其中找到属于自己的编程乐趣!

  • 在Windows 64位系统上从零开始安装Common Lisp的指南

    在这个快速变化的科技世界中,编程语言的选择就像选择早餐一样重要,但却更加复杂。今天,我们要一起踏上安装Common Lisp的旅程,特别是在Windows 64位系统上。无论你是编程新手,还是经验丰富的老手,这个指南都将为你提供清晰的步骤,确保你能够顺利地开始你的Lisp之旅。

    🏗️ 准备工作:下载与安装

    首先,你需要为你的编程环境搭建一个坚实的基础。以下是安装Common Lisp所需的几个主要组件。

    1. 下载并安装rho-emacs

    rho-emacs是一个轻量级的Emacs版本,非常适合Lisp开发。你可以从这里下载它。在安装时,选择你的“主文件夹”为C. \Users\你的用户名。这一步就像为你的编程之旅选一个舒适的座椅。

    2. 安装SBCL

    SBCL(Steel Bank Common Lisp)是一个广泛使用的Common Lisp实现。你可以从SBCL官网下载适合Windows的版本。安装SBCL就像给你的电脑装上了一个强大的大脑。

    3. 获取64位GIT

    GIT不仅是一个版本控制工具,它还会为你安装一些必需的SSL文件和基本的Unix工具,比如bash。访问GIT官网下载并安装。安装时,请选择行结尾设置为“checkout as-is, commit unix-style line endings”,这样SBCL在处理文件时会更顺畅。

    4. 获取SQLite DLL

    为了让你的Lisp程序能够与数据库无缝连接,你需要下载64位SQLite DLL。访问SQLite官网,下载后将内容复制到C. \Program Files\Git\mingw64\bin目录。

    5. 下载QuickLisp

    打开Git Bash(从Windows应用中找到),然后运行以下命令来下载QuickLisp:

    cd
    curl -o /tmp/ql.lisp http://beta.quicklisp.org/quicklisp.lisp

    QuickLisp就像Lisp的超市,让你轻松获取各种库和工具。

    🚀 快速启动:安装与配置

    现在,我们已经准备好了所有必要的工具,接下来就是安装与配置QuickLisp的过程了。

    6. 安装QuickLisp

    在Git Bash中继续运行以下命令:

    sbcl --no-sysinit --no-userinit --load /tmp/ql.lisp \
        --eval '(quicklisp-quickstart:install :path "~/.quicklisp")' \
        --eval '(ql:add-to-init-file)' \
        --quit
    sbcl --eval '(ql:quickload :quicklisp-slime-helper)' --quit

    这一步骤像是给你的Lisp环境注入了强心针,让它变得更加灵活和强大。

    7. 配置Emacs

    运行rho-emacs(建议将其添加到PATH或创建一个脚本),输入以下命令:

    /c/Program\ Files/rho-emacs/rho

    使用C-x-f创建文件~/.emacs.d/init.el,并添加以下内容:

    (load (expand-file-name "~/.quicklisp/slime-helper.el"))
    (setq inferior-lisp-program "sbcl")

    这是让Emacs与SBCL连接的桥梁。

    8. 安装UltraLisp

    UltraLisp是一个集合了许多现代Lisp软件的发行版,安装它可以让你更轻松地访问各种工具和库。运行以下命令:

    sbcl --eval '(ql-dist:install-dist "http://dist.ultralisp.org/" :prompt nil)' --eval '(ql:update-all-dists)' --quit

    此时,你的Lisp环境正在逐步壮大,像一棵茁壮成长的树。

    🧩 完成安装:CLOG与Emacs的结合

    9. 安装CLOG

    CLOG是一个功能丰富的Lisp IDE,可以与Emacs无缝集成。运行如下命令来安装它并启动CLOG构建器:

    sbcl --eval '(ql:quickload :clog/tools)' --eval '(clog-tools:clog-builder)'

    这一步将你的编程环境提升到了一个新的高度,使得编写Lisp代码变得轻松愉快。

    10. 重启Emacs

    在完成所有这些步骤后,别忘了重启Emacs。可以使用C-x C-y退出Emacs,然后重新启动:

    /c/Program\ Files/rho-emacs/rho

    接下来,运行Slime,使用M-x slime命令。

    📝 注意事项

    在Windows上安装时,可能会遇到一些DLL文件的问题。别担心,你可以从这里下载所需的DLL:clogframe.zip,并使用clograme.exe与CLOG配合使用。

    结尾

    恭喜你!现在你已经在Windows 64位系统上成功安装了Common Lisp和相关工具。希望这个指南能帮助你更好地理解Lisp编程的乐趣。就像一场美妙的音乐会,编程的每一个音符都在等待你去谱写。

    参考文献

    1. rho-emacs
    2. SBCL
    3. GIT
    4. SQLite
    5. QuickLisp

    如缺少openssl,则需要安装:Win32/Win64 OpenSSL Installer for Windows – Shining Light Productions (slproweb.com)

    https://slproweb.com/download/Win64OpenSSL_Light-3_3_2.msi

    并合理配置路径,使得sbcl能找到对应的DLL。


  • CLOG:构建现代用户界面的全新框架

    🌐 CLOG的概念目的

    在这个信息爆炸的时代,构建用户友好的界面就像在一片沙漠中找到水源一样重要。CLOG——一个为图形用户界面提供的框架,旨在利用最新的网络技术,帮助开发者创造出与众不同的应用程序。无论是小型单用户工具还是庞大的多用户并发应用,CLOG都能提供一个跨平台的解决方案,让每一个应用的界面都闪闪发光。

    🚀 底层技术

    运输机制

    CLOG的通信机制可谓是现代技术的结晶。它采用了客户端/服务器架构,初始引导使用HTTP协议,而后续通信则依赖于WebSocket。简单来说,WebSocket就像是CLOG的“高速公路”,使得数据在服务器和客户端之间快速流动。

    在CLOG应用的启动阶段,一个HTML引导页面和JavaScript脚本会帮助建立WebSocket连接。一旦连接建立,所有的消息交换都在JavaScript的控制下进行,这一切对于用户来说是透明的,仿佛魔术般的无缝体验。

    用户界面的初始状态建立

    在CLOG中,建立用户界面的初始状态就像调制一杯完美的鸡尾酒,有三种主要的方式:

    1. 使用初始引导文件:就像传统的网络服务器一样,CLOG允许通过HTTP服务初始UI。开发者可以将任何包含boot.js脚本的HTML文件作为引导文件,轻松实现服务器端的控制。
    2. 使用CLOG API从零开始构建UI:对于那些喜欢DIY的开发者,CLOG提供了一套API,允许他们从头开始打造完美的界面,简直是程序员的天堂。
    3. 混合方法:CLOG Builder面板可以创建复合组件,通过批量写入HTML/JS到浏览器,实现更高效的UI构建。

    性能考量

    在性能方面,方法1和3相较于Ajax性能优越,而方法2则稍显逊色,但对于完全无HTML和无JS代码的应用场景,依然具有其独特的优势。

    ✨ 初始化后的操作

    CLOG不仅仅是一个框架,它还是一个事件驱动的系统。从CLOG 1.2版本开始,新增的“呈现”系统允许Lisp对象和CLOG对象之间的双向绑定,使得数据模型和业务逻辑的使用变得更加自然,几乎不需要依赖CLOG API。

    CLOG应用可以完全无状态运行,利用post/get与任何其他Web框架一样,并且能够像传统应用一样进行状态管理。而更重要的是,CLOG赋予了开发者更高的响应性,使得应用能够随时随地与用户互动。

    可靠性

    在可靠性方面,CLOG比传统的客户端/服务器配置更具优势。它的设计旨在应对不理想的环境,确保即便在中断的情况下也能快速恢复。虽然完全的断开连接需要在设计中加以考虑,但CLOG依然能在同一台机器上实现连接的自动恢复。

    📈 可扩展性

    CLOG的可扩展性广泛,既可以用于提供静态页面,也可以构建服务器端生成的页面,甚至是客户端页面的服务器控制构建设计。无论选择哪种方式,CLOG的可扩展性都与其他基于Web的系统相当。

    然而,CLOG独特之处在于它能够开发出支持并发用户交互的系统,适用于大型商业应用或任何需要实时信息传递的应用。这样的能力使得CLOG能够更接近传统的客户端/服务器应用的扩展性。

    🏁 结论

    感谢您深入了解CLOG的工作原理。希望本文能为您提供清晰的见解,帮助您理解CLOG目前的能力以及它的不足之处。如果您有任何需求,请随时联系我,让CLOG能够满足更广泛的应用场景,从单用户工具到大规模的并发多用户系统,助力于每一个开发者的梦想成真。


    参考文献

    1. CLOG Technical Overview and Purpose. GitHub Repository
    2. WebSocket Protocol. RFC 6455
    3. JavaScript: Understanding the Weird Parts. Udemy Course
    4. Event-Driven Programming in JavaScript. MDN Web Docs
    5. Scalable Web Applications: The Future of Software Development. Article
人生梦想 - 关注前沿的计算机技术 acejoy.com