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

unj*_*nj2 144 lisp scheme clojure common-lisp paul-graham

我需要一些帮助来理解Paul Graham的What What Lisp Different中的一些观点.

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

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

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

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

这些点意味着什么?它们在C或Java等语言中有何不同?除了Lisp系列语言之外的任何其他语言现在都有这些结构吗?

Mic*_*zyk 97

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"))
Run Code Online (Sandbox Code Playgroud)

这段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目前的实现方式一样,图片与保罗·格雷厄姆的想法有些不同,尽管聪明的黑客可能有任何可能.在Common Lisp中,故事在概念上会略微清晰.然而,基本知识是相似的:Lisp Reader是一个状态机,除了执行状态转换并最终声明它是否已达到"接受状态"之外,还会吐出字符所代表的Lisp数据结构.因此,字符123变为数字123等.现在重要的一点是:这个状态机可以通过用户代码修改.(如前所述,在CL的情况下这是完全正确的;对于Clojure来说,需要进行黑客攻击(不鼓励并且不在实践中使用).但我离题了,这是PG的文章,我应该详细阐述,所以...)

所以,如果你是一个Common Lisp程序员并且你碰巧喜欢Clojure风格的矢量文字的想法,你可以插入一个函数来对某些字符序列作出适当的反应 - [或者#[可能 - 并将其视为矢量文字的开头以匹配结束].这样的函数被称为读取器宏,就像常规宏一样,它可以执行任何类型的Lisp代码,包括本身用以前注册的读取器宏启用的时髦符号编写的代码.因此,您可以在阅读时阅读整个语言.

把它包装起来:

实际上,到目前为止已经证明的是,可以在读取时或编译时运行常规Lisp函数; 一步到位需要从这里开始理解读取和编译本身在读取,编译或运行时是否可行,就是要意识到读取和编译本身是由Lisp函数执行的.您可以随时调用readeval随时从字符流中读取Lisp数据或分别编译和执行Lisp代码.这就是整个语言,一直都在那里.

注意Lisp从列表中满足point(3)的事实对于它设法满足point(4)的方式至关重要--Lisp提供的宏的特殊风格严重依赖于由常规Lisp数据表示的代码,这是(3)启用的东西.顺便说一句,在这里只有代码的"树状"方面才是真正重要的 - 你可以想象有一个使用XML编写的Lisp.

  • 小心:通过说"常规(编译)宏",你接近于暗示编译器宏是"常规"宏,而在Common Lisp(至少)中,"编译器宏"是一个非常具体和不同的东西:http: //www.lispworks.com/documentation/lw51/CLHS/Body/26_glo_c.htm#compiler_macro (4认同)

Rai*_*wig 65

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

(defun print-twice (it)
  (print it)
  (print it))
Run Code Online (Sandbox Code Playgroud)

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

数据对象具有"类型",并且可以查询所有数据对象的"类型".

(type-of "abc")  -> STRING
Run Code Online (Sandbox Code Playgroud)

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

符号是具有名称的数据对象.通常名称可用于查找对象:

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

(find-symbol "SIN")   ->  SIN
Run Code Online (Sandbox Code Playgroud)

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

(eq 'sin 'cos) -> NIL
(eq 'sin 'sin) -> T
Run Code Online (Sandbox Code Playgroud)

这允许我们例如用符号写一个句子:

(defvar *sentence* '(mary called tom to tell him the price of the book))
Run Code Online (Sandbox Code Playgroud)

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

(count 'the *sentence*) ->  2
Run Code Online (Sandbox Code Playgroud)

在Common Lisp中,符号不仅具有名称,而且还可以具有值,函数,属性列表和包.因此,符号可用于命名变量或函数.属性列表通常用于将元数据添加到符号.

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

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

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

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

(length '(* 3 (+ 2 5))) -> 3
Run Code Online (Sandbox Code Playgroud)

那个树:

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

[*|*]--->[*|*]--->[*|*]--->NIL
 |        |        |
 v        v        v
 *        3       [*|*]--->[*|*]--->[*|*]--->NIL
                   |        |        |
                   v        v        v
                   +        2        5
Run Code Online (Sandbox Code Playgroud)

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

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

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

它们在C或Java等语言中有何不同?

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

LISP家族语言以外的任何其他语言现在都有这些结构吗?

许多语言都具有这些功能.

区别:

在Lisp中,这些功能被设计为语言,因此易于使用.


Mat*_*tis 33

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

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

(4)以C为例:该语言实际上是两种不同的子语言:if()和while()以及预处理器.您使用预处理器来保存不必一直重复自己,或者使用#if/#ifdef跳过代码.但是这两种语言是完全分开的,你不能像编译时那样在编译时使用while().

C++使模板更糟糕.查看一些关于模板元编程的参考资料,它提供了一种在编译时生成代码的方法,对于非专家来说非常困难.此外,它实际上是使用模板和宏的一堆黑客和技巧,编译器无法提供一流的支持 - 如果您发出简单的语法错误,编译器无法给您一个明确的错误消息.

好吧,使用Lisp,你可以用一种语言完成所有这些.在第一天学习时,您可以使用相同的东西在运行时生成代码.这并不是说元编程是微不足道的,但是对于第一类语言和编译器支持来说它肯定更直接.

  • 我认为Java变量根本不像Lisp符号.在Java中没有符号表示符号,你可以用变量做的唯一事情就是得到它的值单元格.字符串可以被实习,但它们通常不是名称,所以谈论它们是否可以被引用,评估,传递等等甚至都没有意义. (9认同)
  • 哦,这个功能(和简单性)现在已经超过50年了,并且很容易实现,新手程序员可以用最少的指导来解决它,并学习语言基础知识.作为一个优秀的初学者项目,您不会听到类似的Java,C,Python,Perl,Haskell等声称! (7认同)
  • @Dan - 不确定何时将第一个实现放在一起,但是关于符号计算的最初[麦卡锡论文](http://www-formal.stanford.edu/jmc/recursive.html)于1960年出版. (3认同)
  • 超过40岁可能更准确:),@ Ken:我认为他的意思是1)java中的非原始变量是通过refence,类似于lisp和2)java中的实习字符串类似于lisp中的符号 - 当然,就像你说的那样,你不能在Java中引用或评估实际的字符串/代码,所以它们仍然完全不同. (2认同)