分享
为什么问答平台  ›  专栏  ›  技术社区  ›  kunj2aan

请解释一下保罗·格雷厄姆在Lisp上的一些观点 - Please explain some of Paul Graham's points on Lisp

  •  141
  • kunj2aan  · 技术社区  · 1 周前

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

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

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

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

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

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

    4 回复  |  直到 3 年前
        1
  •  97
  •   Michał Marczyk    3 年前

    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+" 文字(更多内容见下文)。一些口齿不清的人在这个过程中加入了他们自己的小麻烦,但保罗·格雷厄姆主要指的是普通的口齿不清。在与你的问题有关的问题上,clojure与cl类似。

    编译时的整个语言:

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

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

    阅读时的整个语言:

    让我们回到那个 “\d+” 正则表达式。如上所述,在编译器听到第一次提到准备编译的新代码之前,它将在读取时转换为实际的已编译模式对象。这是怎么发生的?

    好吧,Clojure目前的实现方式,这张图与Paul Graham的想法有些不同,尽管任何事情都有可能 a clever hack . 在普通的Lisp中,这个故事在概念上会稍微清晰一些。然而,基本原理是相似的:lisp reader是一个状态机,除了执行状态转换并最终声明它是否已达到“接受状态”,它还吐出字符所代表的lisp数据结构。因此人物 123 成为数字 一百二十三 等等,现在重要的一点是: 此状态机可以由用户代码修改 . (如前所述,这在cl的情况下是完全正确的;对于clojure,需要黑客(不鼓励在实践中使用)。但我离题了,这是PG的文章,我要详细阐述,所以…)

    因此,如果您是一个普通的Lisp程序员,并且您碰巧喜欢Clojure风格的向量文本,那么您可以将一个函数插入到阅读器中,以便对某些字符序列做出适当的反应。-- [ #[ 可能——并将其视为矢量文字的开头,以匹配结束 ] . 这种函数称为 阅读器宏 和常规宏一样,它可以执行任何类型的lisp代码,包括用以前注册的读卡器宏启用的时髦符号编写的代码。所以在阅读的时候有你的全部语言。

    包装起来:

    实际上,到目前为止已经证明,人们可以在读取或编译时运行常规的lisp函数;我们需要从这里了解如何在读取、编译或运行时进行读取和编译,这一步是认识到读取和编译本身是由lisp函数执行的。你可以打电话 read eval 可以随时从字符流中读取Lisp数据,也可以分别编译和执行Lisp代码。这就是所有的语言。

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

        2
  •  64
  •   Rainer Joswig    8 年前

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

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

    “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
    

    在通常情况下,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提供从文本中读取数据和代码、加载到加载代码、评估代码、编译到编译代码以及打印到写入数据和代码到文本的功能。

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

    它们在C语言或Java语言中有什么不同?

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

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

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

    差异:

    在Lisp中,这些功能被设计成语言,以便使用。

        3
  •  31
  •   Matt Curtis    8 年前

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

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

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

    C++使模板变得更加糟糕。查看一些关于模板元编程的参考资料,它提供了一种在编译时生成代码的方法,对于非专家来说,这是非常困难的。此外,使用模板和宏确实是一堆黑客和把戏,编译器不能提供一流的支持——如果你犯了一个简单的语法错误,编译器就不能给你一个明确的错误信息。

    好吧,有了Lisp,所有这些都是用一种语言实现的。您在运行时使用与第一天学习相同的东西来生成代码。这并不意味着元编程是微不足道的,但在一流的语言和编译器支持下,它肯定更简单。

        4
  •  -3
  •   CyberFonic    8 年前

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

    在方案中,所有事物都以类似的方式被视为一个对象。我不知道常见的口齿不清。我会尽量避免用C/C++概念来思考。当我试图让我的头绕开美丽而简单的口齿不清时,他们让我放慢了脚步。