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

发布于 2024-08-30 22:57:22 字数 1435 浏览 6 评论 0原文

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

一个人练习一个人 2024-09-06 22:57:23

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+" 文字相对应的单个编译的正则表达式模式对象(更多内容见下文)。一些 Lisp 在这个过程中添加了自己的小改动,但 Paul Graham 主要指的是 Common Lisp。在与您的问题相关的方面,Clojure 与 CL 类似。

编译时的整个语言:

在此之后,所有编译器处理的都是 Lisp 程序员习惯于操作的 Lisp 数据结构(对于 Lisp 解释器来说也是如此;Clojure 代码总是被编译)。此时,一个奇妙的可能性变得显而易见:为什么不允许 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 函数执行的。您可以随时调用readeval从字符流中读取Lisp数据或编译&分别执行 Lisp 代码。这就是整个语言,一直存在。

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

Matt's explanation is perfectly fine -- and he takes a shot at a comparison to C and Java, which I won't do -- but for some reason I really enjoy discussing this very topic once in a while, so -- here's my shot at an answer.

On points (3) and (4):

Points (3) and (4) on your list seem the most interesting and still relevant now.

To understand them, it is useful to have a clear picture of what happens with Lisp code -- in the form of a stream of characters typed in by the programmer -- on its way to being executed. Let's use a concrete example:

;; 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"))

This snippet of Clojure code prints out aFOObFOOcFOO. Note that Clojure arguably does not fully satisfy the fourth point on your list, since read-time is not really open to user code; I will discuss what it would mean for this to be otherwise, though.

So, suppose we've got this code in a file somewhere and we ask Clojure to execute it. Also, let's assume (for the sake of simplicity) that we've made it past the library import. The interesting bit starts at (println and ends at the ) far to the right. This is lexed / parsed as one would expect, but already an important point arises: the result is not some special compiler-specific AST representation -- it's just a regular Clojure / Lisp data structure, namely a nested list containing a bunch of symbols, strings and -- in this case -- a single compiled regex pattern object corresponding to the #"\d+" literal (more on this below). Some Lisps add their own little twists to this process, but Paul Graham was mostly referring to Common Lisp. On the points relevant to your question, Clojure is similar to CL.

The whole language at compile time:

After this point, all the compiler deals with (this would also be true for a Lisp interpreter; Clojure code happens always to be compiled) is Lisp data structures which Lisp programmers are used to manipulating. At this point a wonderful possibility becomes apparent: why not allow Lisp programmers to write Lisp functions which manipulate Lisp data representing Lisp programmes and output transformed data representing transformed programmes, to be used in place of the originals? In other words -- why not allow Lisp programmers to register their functions as compiler plugins of sorts, called macros in Lisp? And indeed any decent Lisp system has this capacity.

So, macros are regular Lisp functions operating on the programme's representation at compile time, before the final compilation phase when actual object code is emitted. Since there are no limits on the kinds of code macros are allowed to run (in particular, the code which they run is often itself written with liberal use of the macro facility), one can say that "the whole language is available at compile time".

The whole language at read time:

Let's go back to that #"\d+" regex literal. As mentioned above, this gets transformed to an actual compiled pattern object at read time, before the compiler hears the first mention of new code being prepared for compilation. How does this happen?

Well, the way Clojure is currently implemented, the picture is somewhat different than what Paul Graham had in mind, although anything is possible with a clever hack. In Common Lisp, the story would be slightly cleaner conceptually. The basics are however similar: the Lisp Reader is a state machine which, in addition to performing state transitions and eventually declaring whether it has reached an "accepting state", spits out Lisp data structures the characters represent. Thus the characters 123 become the number 123 etc. The important point comes now: this state machine can be modified by user code. (As noted earlier, that's entirely true in CL's case; for Clojure, a hack (discouraged & not used in practice) is required. But I digress, it's PG's article I'm supposed to be elaborating on, so...)

So, if you're a Common Lisp programmer and you happen to like the idea of Clojure-style vector literals, you can just plug into the reader a function to react appropriately to some character sequence -- [ or #[ possibly -- and treat it as the start of a vector literal ending at the matching ]. Such a function is called a reader macro and just like a regular macro, it can execute any sort of Lisp code, including code which has itself been written with funky notation enabled by previously registered reader macros. So there's the whole language at read time for you.

Wrapping it up:

Actually, what has been demonstrated thus far is that one can run regular Lisp functions at read time or compile time; the one step one needs to take from here to understanding how reading and compiling are themselves possible at read, compile or run time is to realise that reading and compiling are themselves performed by Lisp functions. You can just call read or eval at any time to read in Lisp data from character streams or compile & execute Lisp code, respectively. That's the whole language right there, all the time.

Note how the fact that Lisp satisfies point (3) from your list is essential to the way in which it manages to satisfy point (4) -- the particular flavour of macros provided by Lisp heavily relies on code being represented by regular Lisp data, which is something enabled by (3). Incidentally, only the "tree-ish" aspect of the code is really crucial here -- you could conceivably have a Lisp written using XML.

情深如许 2024-09-06 22:57:23

1) 变量的新概念。在 Lisp 中,所有变量实际上都是指针。值是有类型的,而不是变量,分配或绑定变量意味着复制指针,而不是它们指向的内容。

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

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

数据对象具有“类型”,并且可以查询所有数据对象的“类型”。

(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))

现在我们可以计算句子:

(count 'the *sentence*) ->  2

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

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

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

列表 (* 3 2) 可以是数据和代码:

(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) A new concept of variables. In Lisp, all variables are effectively pointers. Values are what have types, not variables, and assigning or binding variables means copying pointers, not what they point to.

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

'it' is a variable. It can be bound to ANY value. There is no restriction and no type associated with the variable. If you call the function, the argument does not need to be copied. The variable is similar to a pointer. It has a way to access the value that is bound to the variable. There is no need to reserve memory. We can pass any data object when we call the function: any size and any type.

The data objects have a 'type' and all data objects can be queried for its 'type'.

(type-of "abc")  -> STRING

2) A symbol type. Symbols differ from strings in that you can test equality by comparing a pointer.

A symbol is a data object with a name. Usually the name can be used to find the object:

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

(find-symbol "SIN")   ->  SIN

Since symbols are real data objects, we can test whether they are the same object:

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

This allows us for example to write a sentence with symbols:

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

Now we can count the number of THE in the sentence:

(count 'the *sentence*) ->  2

In Common Lisp symbols not only have a name, but they also can have a value, a function, a property list and a package. So symbols can be used to name variables or functions. The property list is usually used to add meta-data to symbols.

3) A notation for code using trees of symbols.

Lisp uses its basic data structures to represent code.

The list (* 3 2) can be both data and code:

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

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

The tree:

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

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

4) The whole language always available. There is no real distinction between read-time, compile-time, and runtime. You can compile or run code while reading, read or run code while compiling, and read or compile code at runtime.

Lisp provides the functions READ to read data and code from text, LOAD to load code, EVAL to evaluate code, COMPILE to compile code and PRINT to write data and code to text.

These functions are always available. They don't go away. They can be part of any program. That means any program can read, load, eval or print code - always.

How are they different in languages like C or Java?

Those languages don't provide symbols, code as data or runtime evaluation of data as code. Data objects in C are usually untyped.

Do any other languages other than LISP family languages have any of these constructs now?

Many languages have some of these capabilities.

The difference:

In Lisp these capabilities are designed into the language so that they are easy to use.

香橙ぽ 2024-09-06 22:57:23

对于第(1)点和第(2)点,他是在历史性地谈论。 Java 的变量几乎相同,这就是为什么你需要调用 .equals() 来比较值。

(3) 谈论的是S-表达式。 Lisp 程序是用这种语法编写的,与 Java 和 C 等临时语法相比,它提供了许多优势,例如以比 C 宏或 C++ 模板更清晰的方式捕获宏中的重复模式,以及使用相同的核心列表操作代码您用于数据的操作。

(4) 以 C 为例:该语言实际上是两种不同的子语言:诸如 if() 和 while() 之类的东西,以及预处理器。您可以使用预处理器来避免一直重复自己,或者使用#if/#ifdef 跳过代码。但这两种语言是完全独立的,你不能像 #if 那样在编译时使用 while() 。

C++ 使用模板使情况变得更糟。查看一些有关模板元编程的参考资料,它提供了一种在编译时生成代码的方法,对于非专家来说很难理解。此外,这实际上是一堆使用模板和宏的技巧和技巧,编译器无法提供一流的支持 - 如果您犯了一个简单的语法错误,编译器将无法给您明确的错误消息。

好吧,有了 Lisp,你就可以用一种语言来实现这一切。您可以使用与第一天学习相同的内容在运行时生成代码。这并不是说元编程很简单,但有了一流的语言和编译器支持,它肯定会更加简单。

For points (1) and (2), he is talking historically. Java's variables are pretty much the same, which is why you need to call .equals() to compare values.

(3) is talking about S-expressions. Lisp programs are written in this syntax, which provides lots of advantages over ad-hoc syntax like Java and C, such as capturing repeated patterns in macros in a far cleaner way than C macros or C++ templates, and manipulating code with the same core list operations that you use for data.

(4) taking C for example: the language is really two different sub languages: stuff like if() and while(), and the preprocessor. You use the preprocessor to save having to repeat yourself all the time, or to skip code with #if/#ifdef. But both languages are quite separate, and you can't use while() at compile time like you can #if.

C++ makes this even worse with templates. Check out a few references on template metaprogramming, which provides a way of generating code at compile time, and is extremely difficult for non-experts to wrap their heads around. In addition, it's really a bunch of hacks and tricks using templates and macros that the compiler can't provide first class support for - if you make a simple syntax error, the compiler is unable to give you a clear error message.

Well, with Lisp, you have all this in one single language. You use the same stuff to generate code at run time as you learn in your first day. This isn't to suggest metaprogramming is trivial, but it is certainly more straightforward with first class language and compiler support.

油焖大侠 2024-09-06 22:57:23

第 (1) 点和第 (2) 点也适合 Python。举一个简单的例子“a = str(82.4)”,解释器首先创建一个值为 82.4 的浮点对象。然后它调用一个字符串构造函数,该构造函数返回一个值为“82.4”的字符串。左侧的“a”只是该字符串对象的标签。原始浮点对象已被垃圾回收,因为不再有对其的引用。

在Scheme中,一切都以类似的方式被视为对象。我不确定 Common Lisp。我会尽量避免用 C/C++ 概念来思考。当我试图理解 Lisps 的美丽简单性时,它们大大减慢了我的速度。

Points (1) and (2) would also fit Python. Taking a simple example "a = str(82.4)" the interpreter first creates a floating point object with value 82.4. Then it calls a string constructor which then returns a string with value '82.4'. The 'a' on the left hand side is merely a label for that string object. The original floating point object was garbage collected because there are no more references to it.

In Scheme everything is treated as an object in a similar manner. I'm not sure about Common Lisp. I would try to avoid thinking in terms of C/C++ concepts. They slowed me down heaps when I was trying to get my head around the beautiful simplicity of Lisps.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文