请解释保罗·格雷厄姆(Paul Graham)关于Lisp的一些观点


146

我需要一些帮助来了解Paul Graham的《What Lisp Lisp Different》中的一些要点。

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

  2. 符号类型。符号与字符串的不同之处在于,您可以通过比较指针来测试是否相等。

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

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

这些要点是什么意思?它们在诸如C或Java之类的语言中有何不同?除Lisp家族语言外,其他语言现在是否具有这些构造?


10
我不确定这里是否需要功能性编程标签,因为在许多Lisps中编写命令式代码或OO代码与编写功能性代码一样是可能的-实际上,有很多非功能性Lisp周围的代码。我建议您删除fp标签并添加clojure-希望这可能会带来基于JVM的Lispers的一些有趣的输入。
米哈尔Marczyk

58
我们paul-graham在这里也有标签吗?很棒...
missingfaktor

@missingfaktor也许它需要一个简单的要求
cat

Answers:


98

马特(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解释器也是如此; Clojure代码总是被编译)是Lisp程序员用来操作的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函数执行的。您可以随时调用readeval随时从字符流中读取Lisp数据,或分别编译和执行Lisp代码。这就是所有时间的所有语言。

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


4
小心:通过说“常规(编译器)宏”,您几乎暗示编译器宏是“常规”宏,而在Common Lisp中(至少),“编译器宏”是非常特定且不同的东西:lispworks。 com / documentation / lw51 / CLHS / Body /…

肯:好的,谢谢!我将其更改为“常规宏”,我认为这不太可能使任何人绊倒。
米哈尔Marczyk

很棒的答案。我在5分钟内从中了解到的知识要比在几个小时里仔细研究/思考问题的时间要多。谢谢。
查理·弗罗里斯

编辑:啊,误解了一个句子。更正了语法(需要“同行”接受我的编辑)。
Tatiana Racheva 2011年

S表达式和XML可以指示相同的结构,但是XML冗长得多,因此不适合用作语法。
Sylwester

66

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

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

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

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

(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使用其基本数据结构来表示代码。

列表(* 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提供以下功能:读取以从文本中读取数据和代码,加载以加载代码,EVAL评估代码,COMPILE以编译代码,PRINT以写入数据和代码为文本。

这些功能始终可用。他们不会消失。它们可以是任何程序的一部分。这意味着任何程序都可以始终读取,加载,评估或打印代码。

它们在诸如C或Java之类的语言中有何不同?

这些语言不提供符号,代码作为数据或数据的运行时评估作为代码。C语言中的数据对象通常是无类型的。

除了LISP家族语言之外,其他语言现在是否具有这些构造?

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

区别:

在Lisp中,这些功能是为语言设计的,因此易于使用。


33

对于第(1)和第(2)点,他一直在讲历史。Java的变量几乎相同,这就是为什么您需要调用.equals()来比较值的原因。

(3)在谈论S表达式。Lisp程序是使用这种语法编写的,与Java和C这样的即席语法相比,它具有许多优点,例如以比C宏或C ++模板更干净的方式捕获宏中的重复模式,以及使用相同的核心列表来处理代码。用于数据的操作。

(4)以C为例:该语言实际上是两种不同的子语言:诸如if()和while()之类的东西以及预处理器。您可以使用预处理程序来节省所有时间的重复,或者使用#if /#ifdef跳过代码。但是两种语言都非常不同,并且不能像#if那样在编译时使用while()。

C ++使用模板使情况更糟。查阅有关模板元编程的一些参考资料,它提供了一种在编译时生成代码的方式,并且对于非专家而言,很难全神贯注。此外,使用模板和宏确实是一堆技巧和窍门,编译器无法为其提供一流的支持-如果您犯了简单的语法错误,则编译器将无法向您提供明确的错误消息。

好吧,有了Lisp,您就可以用一种语言来完成所有这些工作。您在第一天就使用相同的东西在运行时生成代码。这并不是说元编程是微不足道的,但是有了一流的语言和编译器支持,它肯定会更简单。


7
噢,这种功能(和简单性)现在已有50多年的历史了,并且很容易实现,新手程序员可以在最少的指导下将其扩展并了解语言基础。您不会听到Java,C,Python,Perl,Haskell等是优秀初学者的项目的类似说法!
马特·柯蒂斯

9
我认为Java变量根本不像Lisp符号。Java中没有符号的表示法,唯一可以使用变量的就是获取其值单元。字符串可以拘留,但他们不是一般的名字,所以它甚至没有意义的谈论他们是否可以被引用,评估,传递等

2
超过40岁的人可能更准确:),@Ken:我认为他的意思是1)Java中的非原始变量是基于引用的,这与lisp类似,并且2)Java中的内部字符串类似于lisp中的符号-当然,就像您说的那样,您无法在Java中引用或评估嵌入式字符串/代码,因此它们仍然大不相同。

3
@Dan-不知道何时将第一个实现放在一起,但是关于符号计算的最初麦卡锡论文于1960
。– Inaimathi 2011年

Java确实以Foo.class / foo.getClass()的形式对“符号”提供了部分/不定期的支持,即,类型类型的Class <Foo>对象有点类似于–枚举值类似于学位。但是Lisp符号的阴影非常小。
BRPocock 2012年

-3

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

在Scheme中,所有内容都以类似的方式被视为对象。我不确定Common Lisp。我会尽量避免考虑C / C ++概念。当我试图绕过Lisps的美丽简单性时,它们使我放慢了脚步。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.