请解释一下 Paul Graham 关于 Lisp 的一些观点

我需要一些帮助来理解从保罗格雷厄姆的 是什么让 Lisp 与众不同的一些点。

  1. 变量的新概念。在 Lisp 中,所有变量都是有效的指针。值是具有类型的,而不是具有变量的,赋值或绑定变量意味着复制指针,而不是复制指针所指向的内容。

  2. 符号类型。符号不同于字符串,因为可以通过比较指针来测试相等性。

  3. 使用符号树表示代码的符号。

  4. 整个语言总是可用的。读取时、编译时和运行时之间没有真正的区别。您可以在读取代码时编译或运行代码,在编译时读取或运行代码,以及在运行时读取或编译代码。

这些点是什么意思?它们在像 C 或 Java 这样的语言中有什么不同?除了 Lisp 家族语言之外,现在还有其他语言使用这些结构吗?

31391 次浏览

关于第(1)和第(2)点,他讲的是历史。Java 的变量几乎是相同的,这就是为什么需要调用。Equals ()来比较值。

(3)正在讨论 S- 表达式。Lisp 程序是用这种语法编写的,它比 Java 和 C 等特殊语法有很多优点,比如用比 C 宏或 C + + 模板简单得多的方式捕获宏中的重复模式,以及使用与数据相同的核心列表操作来操作代码。

(4)以 C 语言为例: 该语言实际上是两种不同的子语言: if ()和 while () ,以及预处理器。您可以使用预处理器来节省不断重复的时间,或者使用 # if/# ifdef 跳过代码。但是这两种语言是相当独立的,您不能像 # if 那样在编译时使用 while ()。

使用模板时,C + + 会使情况更糟。查看一些关于模板超编程的参考资料,它提供了一种在编译时生成代码的方法,非专业人士很难理解。此外,它实际上是一堆使用模板和宏的技巧,编译器不能提供一流的支持-如果你犯了一个简单的语法错误,编译器不能给你一个明确的错误信息。

在 Lisp 中,所有这些都是用一种语言完成的。您在运行时使用相同的东西来生成代码,就像您在第一天学习一样。这并不是说元编程是琐碎的,但是有了第一类语言和编译器的支持,它肯定会更加直观。

Matt 的解释完全没有问题——他尝试将其与 C 和 Java 进行比较,我不会这样做——但出于某种原因,我真的很喜欢偶尔讨论这个话题,所以——这是我的一个尝试。

关于第(3)及(4)点:

在你的清单上的(3)和(4)点似乎是最有趣的,现在仍然相关。

为了理解它们,清楚地了解 Lisp 代码在执行过程中发生了什么是很有用的——以程序员输入的字符流的形式。让我们用一个具体的例子:

;; a library import for completeness,
;; we won't concern ourselves with it
(require '[clojure.contrib.string :as str])


;; this is the interesting bit:
(println (str/replace-re #"\d+" "FOO" "a123b4c56"))

这段 Clojure代码打印出 aFOObFOOcFOO。请注意,Clojure 无法完全满足列表中的第四点,因为读取时并不真正对用户代码开放; 不过,我将讨论如果不这样做将意味着什么。

因此,假设我们将这段代码放在某个文件中,并要求 Clojure 执行它。另外,让我们假设(为了简单起见)我们已经通过了库导入。有趣的部分从 (println开始,到右边远处的 )结束。正如人们所期望的那样,这里进行了词法分析/解析,但是已经出现了一个重要的问题: 结果不是特定于编译器的 AST 表示——只是一个常规的 Clojure/Lisp 数据结构,即一个包含大量符号、字符串的嵌套列表,在本例中,还有一个对应于 #"\d+"文字的单个已编译的正则表达式模式对象(下面将详细介绍)。一些 Lisps 在这个过程中添加了自己的小技巧,但 Paul Graham 主要指的是 Common Lisp。在与您的问题相关的问题上,Clojure 类似于 CL。

编译时的整个语言:

在这之后,所有的编译器处理的都是 Lisp 程序员习惯于操作的 Lisp 数据结构(对于 Lisp 解释器也是如此; Clojure 代码碰巧总是被编译)。在这一点上,一个美妙的可能性变得很明显: 为什么不允许 Lisp 程序员编写 Lisp 函数来操作代表 Lisp 程序的 Lisp 数据和代表转换后的程序的输出转换后的数据,以取代原来的 Lisp 函数呢?换句话说——为什么不允许 Lisp 程序员将他们的函数注册为各种编译器插件,在 Lisp 中称为宏?实际上任何一个像样的 Lisp 系统都有这样的能力。

因此,宏是常规的 Lisp 函数,在编译时,在最后的编译阶段,即实际的对象代码发出之前,对程序的表示进行操作。由于对运行的代码宏的种类没有限制(特别是,它们运行的代码通常是自由使用宏工具编写的) ,因此可以说“整个语言在编译时都是可用的”。

阅读时整个语言:

让我们回到 #"\d+"正则表达式的字面意思。如上所述,在编译器听到第一次提到为编译准备的新代码之前,这将在读取时转换为实际的编译模式对象。怎么会这样?

嗯,目前实现 Clojure 的方式与 Paul Graham 的想法有些不同,尽管 一个聪明的黑客可以做任何事情。在 Common Lisp 中,这个故事在概念上会稍微简洁一些。然而,基本原理是相似的: Lisp Reader 是一个状态机,除了执行状态转换并最终声明它是否已经达到“接受状态”之外,它还显示出字符所代表的 Lisp 数据结构。这样字符 123就变成了数字 123等等。现在重点来了: 此状态机可由用户代码修改。(正如前面提到的,在 CL 的情况下完全正确; 对于 Clojure,需要一个 hack (不鼓励在实践中使用)。但我跑题了,这是 PG 的文章,我应该详细说明,所以...)

所以,如果你是一个 Common Lisp 程序员,并且你碰巧喜欢 Clojure 风格的向量文字的想法,你可以在读者中插入一个函数来对某个字符序列做出适当的反应——可能是 [或者 #[——并且把它当作一个向量文字在匹配的 ]处结束的开始。这样的函数被称为 宏阅读器,就像一个普通的宏一样,它可以执行任何类型的 Lisp 代码,包括那些由之前注册的读宏启用的时髦符号编写的代码。所以你可以在阅读的时候学习整门语言。

总结一下:

实际上,到目前为止已经证明的是,人们可以在读取时或编译时运行普通的 Lisp 函数; 要理解读取和编译本身在读取、编译或运行时是如何可能的,我们需要从这里开始采取的一个步骤是认识到读取和编译本身是由 Lisp 函数执行的。你可以随时调用 read或者 eval从字符流中读取 Lisp 数据,或者分别编译和执行 Lisp 代码。语言就是这样,一直都是。

请注意,Lisp 满足列表中的点(3)这一事实对于它满足点(4)的方式至关重要—— Lisp 提供的宏的特殊风格很大程度上依赖于常规 Lisp 数据所表示的代码,而这正是(3)所启用的。顺便说一句,这里只有代码的“树状”方面是真正关键的——您可以想象使用 XML 编写一个 Lisp。

1) 变量的新概念。在 Lisp 中,所有变量都是有效的指针。值是具有类型的,而不是具有变量的,赋值或绑定变量意味着复制指针,而不是复制指针所指向的内容。

(defun print-twice (it)
(print it)
(print it))

它是一个变量。它可以绑定到任何值。没有限制,也没有与变量关联的类型。如果调用函数,则不需要复制参数。该变量类似于指针。它有一种方法来访问绑定到变量的值。不需要 储备内存。我们可以在调用函数时传递任何数据对象: 任何大小和任何类型。

数据对象有一个“ type”,可以查询所有数据对象的“ type”。

(type-of "abc")  -> STRING

2) 符号类型。符号不同于字符串,因为可以通过比较指针来测试相等性

符号是带有名称的数据对象。通常该名称可用于查找对象:

|This is a Symbol|
this-is-also-a-symbol


(find-symbol "SIN")   ->  SIN

由于符号是真实的数据对象,我们可以测试它们是否是同一个对象:

(eq 'sin 'cos) -> NIL
(eq 'sin 'sin) -> T

这使得我们可以用符号来写一个句子:

(defvar *sentence* '(mary called tom to tell him the price of the book))

现在我们可以计算句子中 THE 的数量:

(count 'the *sentence*) ->  2

在 Common Lisp 中,符号不仅有一个名称,还可以有一个值、一个函数、一个属性列表和一个包。因此,符号可以用来命名变量或函数。属性列表通常用于向符号添加元数据。

3) 使用符号树表示代码的符号

Lisp 使用它的基本数据结构来表示代码。

列表(* 32)可以是数据也可以是代码:

(eval '(* 3 (+ 2 5))) -> 21


(length '(* 3 (+ 2 5))) -> 3

树:

CL-USER 8 > (sdraw '(* 3 (+ 2 5)))


[*|*]--->[*|*]--->[*|*]--->NIL
|        |        |
v        v        v
*        3       [*|*]--->[*|*]--->[*|*]--->NIL
|        |        |
v        v        v
+        2        5

4) 整个语言总是可用的。读取时、编译时和运行时之间没有真正的区别。您可以在读取代码时编译或运行代码,在编译时读取或运行代码,以及在运行时读取或编译代码。

Lisp 提供了 READ 函数从文本中读取数据和代码,LOAD 函数加载代码,EVAL 函数评估代码,COMPILE 函数编译代码,PRINT 函数将数据和代码写入文本。

这些函数总是可用的。它们不会消失。他们可以成为任何项目的一部分。这意味着任何程序都可以读取、加载、计算或打印代码——总是如此。

它们在像 C 或 Java 这样的语言中有什么不同?

这些语言不提供符号、代码作为数据或数据的运行时计算作为代码。C 中的数据对象通常是非类型化的。

除了 LISP 家族语言之外,现在还有其他语言使用这些结构吗?

许多语言都具有其中的一些功能。

区别在于:

在 Lisp 中,这些功能被设计到语言中,因此它们很容易使用。

分(1)和(2)也适合 Python。以“ a = str (82.4)”为例,解释器首先创建一个值为82.4的浮点对象。然后调用字符串构造函数,该构造函数返回值为“82.4”的字符串。左边的‘ a’仅仅是字符串对象的标签。原始浮点对象被垃圾收集,因为没有更多对它的引用。

在 Scheme 中,所有内容都以类似的方式作为对象处理。我对 Common Lisp 不太确定。我会尽量避免从 C/C + + 概念的角度思考问题。当我试图理解 Lisps 的美丽简洁时,他们让我慢了很多。