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

0 0 投票数
Article Rating
订阅评论
提醒
0 评论
最多投票
最新 最旧
内联反馈
查看所有评论
0
希望看到您的想法,请您发表评论x