借一步网
作者:
在
Lisp,一种源自1960年代的编程语言家族,以其独特的语法和功能式编程范式闻名。自问世以来,它一直是编程语言中的一个“怪咖”,但也是一颗璀璨的明珠。那么,为什么我们需要关心 Lisp?让我们来深入探讨一下它的魅力。
Lisp 的语法可以用两个简单的规则来概括:
(name arg1 arg2 ...)
这看起来似乎过于简单,但正是这种简化,使得 Lisp 拥有了强大的表达能力和灵活性。因为代码本质上就是数据,我们可以编写操作代码的代码,这也就是 Lisp 的宏系统的核心。
(let ((x 1) (y 1)) (if (> x 0) (+ x y) y))
上面的代码展示了典型的一段 Lisp 表达式。我们定义了两个局部变量 x 和 y,然后通过一个条件语句检查 x 是否大于 0。如果是,则返回 x + y,否则返回 y。
x
y
x + y
与大多数编程语言不同,Lisp 的宏不仅仅是“函数”。宏可以操作代码本身,接收未求值的代码作为输入,并生成新的代码。通过宏,程序员可以扩展语言的语法,创建适合特定需求的抽象。
让我给你一个简单但生动的例子。假设我们要实现一个 and 函数,接受两个布尔值并返回 true 仅当两个参数都为 true 时。
and
true
(defn my-and (a b) (if a b nil))
这个函数看起来不错,但有个问题:它没有短路求值。也就是说,即使第一个参数为 false,第二个参数也会被求值。而在很多编程语言中,and 操作符会在第一个参数为 false 时直接返回 false,不会再去求值第二个参数。
false
(defmacro and (a b) `(if ,a ,b nil))
在宏版本中,我们通过 defmacro 创建了一个宏。这个宏不会立即对参数求值,而是生成一个 if 语句。如果第一个参数为真,才会求值第二个参数。这样,我们就实现了短路求值。
defmacro
if
宏的强大之处在于它们不仅能生成代码,还可以改变代码的行为。你可以将宏视为“代码生成器”。通过宏,我们可以轻松地定义新的控制结构、流程逻辑,甚至是全新的语法。换句话说,Lisp 使我们不仅可以使用语言,还可以扩展语言。
假设我们想要创建一个宏,它根据某些条件动态生成不同的代码。这在传统编程语言中可能需要复杂的条件判断和代码生成工具,但在 Lisp 中,宏让这一切变得简单自然。
(defmacro when (condition &rest body) `(if ,condition (progn ,@body)))
这个 when 宏类似于 if,但它只在条件为真时执行多行代码。progn 是 Lisp 中的一个特殊形式,表示顺序执行多条语句。通过宏,我们可以将这些操作封装起来,简化代码的编写。
when
progn
现在你可能会问:“其他语言也有函数,为什么 Lisp 的宏如此特别?”我们来看一些常见的语言,比如 Python。当你写下 example(a, b, c) 时,Python 会先求值 a、b 和 c,然后再调用 example 函数。但 Lisp 的宏不同,它不会立即求值,而是直接接收代码并返回修改后的代码。这种能力让 Lisp 编程语言能够以极高的灵活性处理复杂的编程需求。
example(a, b, c)
a
b
c
example
在 Python 中,虽然你可以编写函数,但要扩展语言的语法却十分困难。编写新的控制结构、在运行时动态生成代码,往往需要借助于元编程或复杂的解析工具。而在 Lisp 中,这一切都可以通过宏轻松实现。
由于 Lisp 的代码本质上就是数据,Lisp 拥有非凡的自省能力。你可以在运行时检查代码结构、修改代码,甚至生成新的代码。这种自省能力在调试、元编程和性能优化中尤为有用。
Lisp 以其简洁的语法和强大的宏系统,提供了其他语言难以企及的灵活性和表达能力。通过宏,程序员可以根据需求创建新的控制结构、流程逻辑,甚至是全新的语言特性。而这一切,都只需要遵循 Lisp 的两个核心规则。
所以,为什么选择 Lisp?因为它不仅仅是一门编程语言,更是一个可以被程序员自由定制的工具箱。Lisp 让你不仅是程序的使用者,更是语言的创造者。它能让复杂的问题变得简单,让简单的代码变得优雅。
在编程语言的世界里,Lisp 是一个特殊的存在。它不仅让你写程序,还可以让你写出可以写程序的程序!听起来很酷吧?在 Lisp 的宇宙中,宏(macros)就是这样一个魔法工具,它能让你不仅仅是编写代码,而是设计出新的控制结构和语言特性。今天,我们就来一探 Lisp 宏的奥秘,并探索其背后的思想。
简单来说,宏是通过代码转换(transformation)实现的特殊操作符。宏不是在运行时执行的,而是在编译时就会被展开(expand)为更基础的 Lisp 表达式。换句话说,宏不仅仅是一个函数,它是一个“编译时的函数”,通过转换代码来生成新的代码。
在 Lisp 中,我们用 defmacro 来定义宏。与 defun 类似,但宏的作用是重写代码,而不是返回一个值。让我们来看一个简单的例子:
defun
(defmacro nil! (x) `(setf ,x nil))
这个宏 nil! 会将它的参数设置为 nil。所以,当我们调用 (nil! a) 时,Lisp 实际上会将其展开为 (setf a nil),然后执行。
nil!
nil
(nil! a)
(setf a nil)
> (nil! x) NIL > x NIL
是不是很神奇?我们定义了一个新的操作符 nil!,但它的作用仅仅是把变量设置成 nil。
Lisp 的宏之所以强大,是因为它允许我们在编译时操作代码。这意味着我们可以在编译时对代码进行转换,从而生成更为高效或简洁的代码。在 Lisp 中,宏的展开过程称为“宏展开”(macro-expansion)。要查看宏展开的结果,可以使用 macroexpand-1 函数:
macroexpand-1
> (macroexpand-1 '(nil! x)) (SETF X NIL) T
macroexpand-1 会将宏调用展开为其最终生成的代码。比如上面的例子中,(nil! x) 展开为了 (setf x nil)。
(nil! x)
(setf x nil)
在 Lisp 中,eval 函数可以将列表作为代码求值,但这种做法并不推荐。原因有两点:
eval
let
因此,Lisp 提供了宏作为更优雅的解决方案。宏会在编译时展开成标准的 Lisp 表达式,避免了运行时的开销。
在宏定义中,有两个非常有用的符号:反引号(`)和 逗号(,)。反引号允许你创建一个模板,而逗号则允许你在模板中插入求值结果。
`
,
看看这个例子:
这里的反引号表示整个表达式是一个模板,而逗号用来插入 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 宏的定义如下:
while
(defmacro while (test &rest body) `(do () ((not ,test)) ,@body))
通过使用宏,我们使得代码更为简洁,并避免了重复的代码块。快速排序算法通过递归地将数组分为两部分,并对每部分进行排序,最后返回排序后的数组。
设计宏时,需要注意以下两个问题:
gensym
让我们来看如何解决这两个问题:
(defmacro ntimes (n &rest body) (let ((g (gensym)) (h (gensym))) `(let ((,h ,n)) (do ((,g 0 (+ ,g 1))) ((>= ,g ,h)) ,@body))))
在这个 ntimes 宏中,我们使用了 gensym 生成唯一的符号,避免了变量捕捉的问题。同时,我们也确保参数 n 只会被求值一次,避免多重求值的副作用。
ntimes
n
宏是 Lisp 强大灵活性的核心。通过宏,程序员可以在编译时改写代码,创建新的控制结构,甚至设计出自己的语言特性。宏不仅仅是为了减少代码量,更重要的是它们使得程序更具可读性和可维护性。
正如 Lisp 的哲学所说:“Lisp 是一种可被程序员塑造的语言。”通过宏,程序员可以将 Lisp 变成任何他们想要的样子。
在 Common Lisp 中,逗号 , 通常出现在宏的上下文中,特别是在使用 反引号(backquote,或称“准引用”)时。理解这个符号的关键在于它和反引号的关系。
逗号
`(1 2 ,x 4)
在这个例子里,1、2 和 4 是常量,直接出现在结果列表中,而 ,x 表示“将变量 x 的当前值插入到这个位置”。如果 x 的值是 3,那么这个表达式会被展开为:
1
2
4
,x
3
(1 2 3 4)
(setf ,x nil)
此表达式中的逗号表明它是出现在一个反引号表达式内部的。假设你在一个宏中使用了反引号,x 是一个变量或参数,在宏运行时需要插入到 setf 表达式中。
setf
(defmacro example-macro (x) `(setf ,x nil))
在这个宏中,x 是宏的参数。反引号表示接下来构造一个模板,而 ,x 告诉 Lisp 在这段模板中插入 x 的值。假设你调用这个宏时传入的是变量 a:
(example-macro a)
这个表达式会被展开为:
即,它将 a 的值设置为 nil。
setf ,x nil
在 Common Lisp 中,gensym 是一个生成“符号”的函数,常用于宏编程中。它的主要功能是创建一个全局唯一的、不与现有符号冲突的新符号,通常用于避免变量名冲突。
在编写宏时,宏展开可能会与用户定义的变量名产生冲突。例如,如果你在宏中使用了一个局部变量,但用户在调用宏时也使用了同名的变量,那么可能会导致意料之外的行为。为了解决这种问题,gensym 可以生成唯一的符号,从而避免这种冲突。
gensym 可以简单地调用而不带任何参数,或带一个可选字符串前缀。它返回一个唯一的符号。
(gensym)
这将生成一个唯一的符号,通常像 G1234 这样,其中数字部分是一个自增的计数器。
G1234
(gensym "temp-")
这将生成一个符号,前缀为 temp-,例如 temp-1234。
temp-
temp-1234
宏编程的一个典型例子是使用 gensym 来避免变量捕获。假设我们想编写一个简单的 with-temp-variable 宏,它会创建一个临时变量并执行一些代码:
with-temp-variable
(defmacro with-temp-variable (var &body body) `(let ((,var (gensym))) ,@body))
这个宏在展开时会创建一个临时的 var,并执行 body。不过,如果用户在调用宏时使用了一个与宏内部变量同名的变量,就可能导致冲突。我们可以通过 gensym 来生成一个唯一的符号,避免这种冲突。
var
body
(defmacro with-temp-variable (&body body) (let ((temp-var (gensym "temp-var-"))) `(let ((,temp-var nil)) ,@body)))
在这个宏中,temp-var 是通过 gensym 生成的唯一符号,所以无论用户传入什么样的变量名,都不会与这个临时变量冲突。
temp-var
为了更好地理解 gensym 在宏中的实际用法,下面是一个经典的例子:编写一个 once-only 宏,它确保在宏展开时某个表达式只被求值一次。
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 中。即使用户传入的表达式可能包含副作用,宏也保证了它只会被执行一次。
(+ x 1)
通过 gensym,我们可以确保宏中生成的符号是全局唯一的,从而避免了潜在的变量名冲突问题。
在 Common Lisp 中,符号 @ 出现在宏定义的上下文中,特别是在 反引号(backquote 或 “准引用”)表达式中。它与逗号 , 配合使用,表示将一个列表的所有元素拼接到当前的列表中。它的作用是“解包”一个列表,并将其所有元素插入到目标列表中。
@
,@
假设我们有一个列表 body,其值是 (print 1) (print 2)。
(print 1) (print 2)
(defmacro example (body) `(list ,@body))
当你传入 (print 1) (print 2) 作为 body 时,@ 的作用是将整个 body 列表解包,将其内容直接拼接到 list 调用中。
list
(example ((print 1) (print 2)))
(list (print 1) (print 2))
在这个例子中,body 是一个列表,,@body 表示将 body 列表的元素逐个插入到结果的 list 中,而不是将整个 body 列表作为一个元素插入。
,@body
假设我们在宏中只用 ,body 而不是 ,@body:
,body
(defmacro example (body) `(list ',body))
在这种情况下,body 则作为一个整体被插入,而不会被解包成单独的元素。这里的 ‘ 表示不要求值body。
(list '((print 1) (print 2)))
这里 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
&rest
假设你调用这个宏如下:
(once-only (+ 1 2) (print temp-var) (print (* temp-var 2)))
在这个调用中:
(+ 1 2)
((print temp-var) (print (* temp-var 2)))
宏的展开过程如下:
(let ((G1234 (+ 1 2))) (print G1234) (print (* G1234 2)))
这里,body 中的每个表达式都被解包并插入到 let 表达式的主体中,正是通过 ,@body 实现的。如果没有 @,整个 body 列表会作为一个整体插入,导致语法错误。
这个技巧在宏编程中非常常见,特别是在处理不定数量的表达式时,可以确保宏展开后的代码结构清晰正确。
在 Common Lisp 中,# 符号的使用有多种形式,具体含义取决于上下文。在你提到的 #:G120 这样的符号中,#: 是一种特殊的符号表示法,称为 未绑定符号(uninterned symbol)。让我们详细解释一下这个符号的用途以及 # 在不同场景下的含义。
#
#:G120
#:
#:symbol-name
(eq '#:foo '#:foo) ;; => NIL
在这里,#:foo 是两个不同的未绑定符号,即使它们的名称相同,它们也是不同的对象。因此,eq 比较它们时返回 NIL,表示它们是不同的符号。
#:foo
eq
NIL
普通的符号(interned symbols)会被存储在某个包中(比如 COMMON-LISP-USER::FOO),并且具有全局唯一性。如果你在不同的地方使用相同名称的符号,它们会是同一个符号。
COMMON-LISP-USER::FOO
(eq 'foo 'foo) ;; => T
在这个例子中,foo 是一个包内的符号(interned symbol),因此它们是相同的符号,eq 返回 T。
foo
T
在宏编程中,尤其是在使用 gensym 生成唯一符号时,通常会生成未绑定符号。使用 #: 可以避免命名冲突,因为这些符号不会和其他定义的符号冲突。
gensym 函数通常生成未绑定符号。你可以认为 gensym 就是自动生成了类似 #:G120 这样的符号。
(gensym) ;; => #:G120
这种生成的符号不会和任何现有的符号冲突,因此在宏展开时非常有用,避免了与用户代码中的符号重名问题。
除了 #: 表示未绑定符号外,# 在 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
#| ... |#
#| 这是一段注释 |#
在你的宏定义中,使用了 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))))
当你调用宏时:
这个宏展开为:
(let ((#:G120 (+ 1 2))) (print #:G120) (print (* #:G120 2)))
这里,#:G120 是一个由 gensym 生成的未绑定符号,确保它不会和任何现有符号冲突。
要发表评论,您必须先登录。
Lisp,一种源自1960年代的编程语言家族,以其独特的语法和功能式编程范式闻名。自问世以来,它一直是编程语言中的一个“怪咖”,但也是一颗璀璨的明珠。那么,为什么我们需要关心 Lisp?让我们来深入探讨一下它的魅力。
语法简洁,规则至上 🧩
Lisp 的语法可以用两个简单的规则来概括:
(name arg1 arg2 ...)
。这看起来似乎过于简单,但正是这种简化,使得 Lisp 拥有了强大的表达能力和灵活性。因为代码本质上就是数据,我们可以编写操作代码的代码,这也就是 Lisp 的宏系统的核心。
上面的代码展示了典型的一段 Lisp 表达式。我们定义了两个局部变量
x
和y
,然后通过一个条件语句检查x
是否大于 0。如果是,则返回x + y
,否则返回y
。强大的宏系统 🛠️
与大多数编程语言不同,Lisp 的宏不仅仅是“函数”。宏可以操作代码本身,接收未求值的代码作为输入,并生成新的代码。通过宏,程序员可以扩展语言的语法,创建适合特定需求的抽象。
让我给你一个简单但生动的例子。假设我们要实现一个
and
函数,接受两个布尔值并返回true
仅当两个参数都为true
时。函数版本:🚫
这个函数看起来不错,但有个问题:它没有短路求值。也就是说,即使第一个参数为
false
,第二个参数也会被求值。而在很多编程语言中,and
操作符会在第一个参数为false
时直接返回false
,不会再去求值第二个参数。宏版本:✅
在宏版本中,我们通过
defmacro
创建了一个宏。这个宏不会立即对参数求值,而是生成一个if
语句。如果第一个参数为真,才会求值第二个参数。这样,我们就实现了短路求值。为什么 Lisp 的宏如此强大?💪
宏的强大之处在于它们不仅能生成代码,还可以改变代码的行为。你可以将宏视为“代码生成器”。通过宏,我们可以轻松地定义新的控制结构、流程逻辑,甚至是全新的语法。换句话说,Lisp 使我们不仅可以使用语言,还可以扩展语言。
一个更复杂的例子:动态创建代码 🧙♂️
假设我们想要创建一个宏,它根据某些条件动态生成不同的代码。这在传统编程语言中可能需要复杂的条件判断和代码生成工具,但在 Lisp 中,宏让这一切变得简单自然。
这个
when
宏类似于if
,但它只在条件为真时执行多行代码。progn
是 Lisp 中的一个特殊形式,表示顺序执行多条语句。通过宏,我们可以将这些操作封装起来,简化代码的编写。相比其他语言的优势 🌟
现在你可能会问:“其他语言也有函数,为什么 Lisp 的宏如此特别?”我们来看一些常见的语言,比如 Python。当你写下
example(a, b, c)
时,Python 会先求值a
、b
和c
,然后再调用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
类似,但宏的作用是重写代码,而不是返回一个值。让我们来看一个简单的例子:这个宏
nil!
会将它的参数设置为nil
。所以,当我们调用(nil! a)
时,Lisp 实际上会将其展开为(setf a nil)
,然后执行。是不是很神奇?我们定义了一个新的操作符
nil!
,但它的作用仅仅是把变量设置成nil
。🛠️ 宏的秘密:代码与表达式的转换
Lisp 的宏之所以强大,是因为它允许我们在编译时操作代码。这意味着我们可以在编译时对代码进行转换,从而生成更为高效或简洁的代码。在 Lisp 中,宏的展开过程称为“宏展开”(macro-expansion)。要查看宏展开的结果,可以使用
macroexpand-1
函数:macroexpand-1
会将宏调用展开为其最终生成的代码。比如上面的例子中,(nil! x)
展开为了(setf x nil)
。⚡ 求值与效率问题
在 Lisp 中,
eval
函数可以将列表作为代码求值,但这种做法并不推荐。原因有两点:eval
需要处理原始列表,编译和解释的效率都较低。eval
无法引用局部的词法环境,特别是在let
表达式中。因此,Lisp 提供了宏作为更优雅的解决方案。宏会在编译时展开成标准的 Lisp 表达式,避免了运行时的开销。
🔧 反引号与逗号:宏定义的好帮手
在宏定义中,有两个非常有用的符号:反引号(
`
)和 逗号(,
)。反引号允许你创建一个模板,而逗号则允许你在模板中插入求值结果。看看这个例子:
这里的反引号表示整个表达式是一个模板,而逗号用来插入
x
的值。如果没有反引号和逗号,我们就得手动构建列表,增加了代码的复杂性。🌀 宏的高级用法:快速排序
为了展示宏的强大,我们来看一个复杂的例子:使用宏来实现快速排序(QuickSort)。以下是使用宏的快速排序代码:
在这个例子中,我们使用了
while
宏来简化循环逻辑。while
宏的定义如下:通过使用宏,我们使得代码更为简洁,并避免了重复的代码块。快速排序算法通过递归地将数组分为两部分,并对每部分进行排序,最后返回排序后的数组。
📉 宏的设计原则
设计宏时,需要注意以下两个问题:
gensym
函数生成一个唯一的符号,避免变量名冲突。让我们来看如何解决这两个问题:
在这个
ntimes
宏中,我们使用了gensym
生成唯一的符号,避免了变量捕捉的问题。同时,我们也确保参数n
只会被求值一次,避免多重求值的副作用。🌐 宏的未来:Lisp 的无限可能
宏是 Lisp 强大灵活性的核心。通过宏,程序员可以在编译时改写代码,创建新的控制结构,甚至设计出自己的语言特性。宏不仅仅是为了减少代码量,更重要的是它们使得程序更具可读性和可维护性。
正如 Lisp 的哲学所说:“Lisp 是一种可被程序员塑造的语言。”通过宏,程序员可以将 Lisp 变成任何他们想要的样子。
📚 参考文献
在 Common Lisp 中,逗号
,
通常出现在宏的上下文中,特别是在使用 反引号(backquote,或称“准引用”)时。理解这个符号的关键在于它和反引号的关系。反引号和逗号的用法:
,
):**逗号
会告诉 Lisp 求值某个特定的表达式并将其插入到模板中,而不是把表达式作为字面量。例子
在这个例子里,
1
、2
和4
是常量,直接出现在结果列表中,而,x
表示“将变量x
的当前值插入到这个位置”。如果x
的值是3
,那么这个表达式会被展开为:在上面的例子中:
此表达式中的逗号表明它是出现在一个反引号表达式内部的。假设你在一个宏中使用了反引号,
x
是一个变量或参数,在宏运行时需要插入到setf
表达式中。举个宏的例子:
在这个宏中,
x
是宏的参数。反引号表示接下来构造一个模板,而,x
告诉 Lisp 在这段模板中插入x
的值。假设你调用这个宏时传入的是变量a
:这个表达式会被展开为:
即,它将
a
的值设置为nil
。总结
setf ,x nil
中,逗号意味着x
是一个需要在宏展开时被求值替换的变量,而不是字面量。在 Common Lisp 中,
gensym
是一个生成“符号”的函数,常用于宏编程中。它的主要功能是创建一个全局唯一的、不与现有符号冲突的新符号,通常用于避免变量名冲突。为什么需要
gensym
?在编写宏时,宏展开可能会与用户定义的变量名产生冲突。例如,如果你在宏中使用了一个局部变量,但用户在调用宏时也使用了同名的变量,那么可能会导致意料之外的行为。为了解决这种问题,
gensym
可以生成唯一的符号,从而避免这种冲突。gensym
的基本用法gensym
可以简单地调用而不带任何参数,或带一个可选字符串前缀。它返回一个唯一的符号。1. 不带参数的使用:
这将生成一个唯一的符号,通常像
G1234
这样,其中数字部分是一个自增的计数器。2. 带参数的使用:
这将生成一个符号,前缀为
temp-
,例如temp-1234
。例子
宏编程的一个典型例子是使用
gensym
来避免变量捕获。假设我们想编写一个简单的with-temp-variable
宏,它会创建一个临时变量并执行一些代码:这个宏在展开时会创建一个临时的
var
,并执行body
。不过,如果用户在调用宏时使用了一个与宏内部变量同名的变量,就可能导致冲突。我们可以通过gensym
来生成一个唯一的符号,避免这种冲突。使用
gensym
改进的宏:在这个宏中,
temp-var
是通过gensym
生成的唯一符号,所以无论用户传入什么样的变量名,都不会与这个临时变量冲突。在宏展开中的应用
为了更好地理解
gensym
在宏中的实际用法,下面是一个经典的例子:编写一个once-only
宏,它确保在宏展开时某个表达式只被求值一次。这个宏的作用是:对传入的
var
只求值一次,然后在宏体body
中多次使用这个值。使用gensym
生成的临时变量,避免了与用户传入的变量名冲突。使用示例:
在这个例子中,
(+ x 1)
只会被求值一次,然后结果会存储在temp-var
中。即使用户传入的表达式可能包含副作用,宏也保证了它只会被执行一次。总结
gensym
生成一个唯一的符号,常用于宏编程中,防止变量名冲突。gensym
生成的符号通常形如G1234
。temp-
,生成的符号形如temp-1234
。gensym
是确保临时变量唯一性的重要工具,尤其是在避免捕获用户变量名时很有用。通过
gensym
,我们可以确保宏中生成的符号是全局唯一的,从而避免了潜在的变量名冲突问题。在 Common Lisp 中,符号
@
出现在宏定义的上下文中,特别是在 反引号(backquote 或 “准引用”)表达式中。它与逗号,
配合使用,表示将一个列表的所有元素拼接到当前的列表中。它的作用是“解包”一个列表,并将其所有元素插入到目标列表中。,@
的作用,
:表示在反引号表达式内求值,替换为求值后的结果。@
(,@
):表示将求值结果插入到列表中,并“解包”列表中的每个元素(即将列表的元素逐个插入),而不是将整个列表作为一个元素插入。例子
假设我们有一个列表
body
,其值是(print 1) (print 2)
。当你传入
(print 1) (print 2)
作为body
时,@
的作用是将整个body
列表解包,将其内容直接拼接到list
调用中。宏调用:
宏展开后的结果:
在这个例子中,
body
是一个列表,,@body
表示将body
列表的元素逐个插入到结果的list
中,而不是将整个body
列表作为一个元素插入。对比:用
,
和,@
的区别假设我们在宏中只用
,body
而不是,@body
:在这种情况下,
body
则作为一个整体被插入,而不会被解包成单独的元素。这里的 ‘ 表示不要求值body。宏调用:
宏展开后的结果:
这里
body
作为一个整体被插入结果列表,变成了嵌套列表,而不是将body
的元素直接插入。上面宏中的
,@body
回到上面的宏定义:
&body body
:&body
是 Common Lisp 中的一种用法,类似于&rest
,用于接收不定数量的表达式,并将它们收集成一个列表。这里body
是一个列表,包含宏调用时传入的所有表达式。,@body
:body
是一个列表,,@body
表示将body
列表的所有元素解包,并插入到展开后的代码中。示例展开
假设你调用这个宏如下:
在这个调用中:
var
是(+ 1 2)
,表示我们传递了一个表达式。body
是((print temp-var) (print (* temp-var 2)))
,这将被传递给宏。宏的展开过程如下:
temp-var
被绑定为一个由gensym
生成的唯一符号,例如G1234
。这里,
body
中的每个表达式都被解包并插入到let
表达式的主体中,正是通过,@body
实现的。如果没有@
,整个body
列表会作为一个整体插入,导致语法错误。总结
,@
表示将一个列表的元素解包,并插入到当前上下文中。,@
可以将一个列表的内容逐个插入,而不是将列表本身插入。once-only
宏里,,@body
使得body
的每个表达式都被插入到生成的let
表达式中,保证宏调用的主体能够正确展开。这个技巧在宏编程中非常常见,特别是在处理不定数量的表达式时,可以确保宏展开后的代码结构清晰正确。
在 Common Lisp 中,
#
符号的使用有多种形式,具体含义取决于上下文。在你提到的#:G120
这样的符号中,#:
是一种特殊的符号表示法,称为 未绑定符号(uninterned symbol)。让我们详细解释一下这个符号的用途以及#
在不同场景下的含义。1.
#:G120
中的#:
是什么?#:
前缀表示 未绑定符号(uninterned symbol)。#:symbol-name
这种表示法时,都会创建一个新的、唯一的符号。例子:
在这里,
#:foo
是两个不同的未绑定符号,即使它们的名称相同,它们也是不同的对象。因此,eq
比较它们时返回NIL
,表示它们是不同的符号。与普通符号的区别:
普通的符号(interned symbols)会被存储在某个包中(比如
COMMON-LISP-USER::FOO
),并且具有全局唯一性。如果你在不同的地方使用相同名称的符号,它们会是同一个符号。在这个例子中,
foo
是一个包内的符号(interned symbol),因此它们是相同的符号,eq
返回T
。2. 为什么要使用
#:
?在宏编程中,尤其是在使用
gensym
生成唯一符号时,通常会生成未绑定符号。使用#:
可以避免命名冲突,因为这些符号不会和其他定义的符号冲突。例子:
gensym
gensym
函数通常生成未绑定符号。你可以认为gensym
就是自动生成了类似#:G120
这样的符号。这种生成的符号不会和任何现有的符号冲突,因此在宏展开时非常有用,避免了与用户代码中的符号重名问题。
3.
#
在其他上下文中的含义除了
#:
表示未绑定符号外,#
在 Common Lisp 中还有许多其他用法。以下是一些常见的例子:#'
表示 函数引用,用于引用函数对象。 例子:#\
表示 字符。用于表示单个字符的字面量。 例子:#(
表示 向量。表示一个向量的字面量。 例子:#'
用于函数引用(与function
等价)。 例子:#.
表示 在读取时求值。会在读取时对表达式求值,并返回结果。 例子:#| ... |#
表示 注释块。用于注释掉一段代码。 例子:4. 回到上面的例子
在你的宏定义中,使用了
gensym
来生成唯一符号。gensym
返回的符号类似于#:G120
,这是一个未绑定符号。宏展开后,#:G120
确保不会与用户代码中的任何符号冲突。宏定义:
当你调用宏时:
这个宏展开为:
这里,
#:G120
是一个由gensym
生成的未绑定符号,确保它不会和任何现有符号冲突。总结
#:
:用于表示 未绑定符号(uninterned symbol)。这些符号不会与任何包中的符号冲突,通常用于宏编程中生成唯一的符号。gensym
:是生成未绑定符号的常用方式,避免在宏展开过程中与用户定义的符号产生冲突。#
在 Common Lisp 中有许多其他特殊用途,包括字符、向量、函数引用等。