推荐一篇博文, (希望能)帮助搞懂到底什么是 symbol 和 quote

lisp 里面的symbol 和 quote 概念让我很头疼, 推荐一篇博文, 希望能帮助到初学者. https://stevelosh.com/blog/2016/06/symbolic-computation/

2 个赞

我的解释,在 read-eval-print 循环中,reader 处理字符串得到 form(symbol 是 form 的组成部分,也可以说是最简单的 form),evaluator 计算 form 得到 value,printer 将 value 打印出来(再次得到字符串)。

evaluator 对 symbol 的默认操作是求值,即把 symbol 的 value slot 或者 function slot 里的东西拿出来,替换掉 symbol。

因为 symbol 也是一种 value,现在你想跳过 evaluator 对于某个 symbol 的计算(求值)过程,就需要一个 special form 即 quote,evaluator 看到 quote 这个 form 的时候便会把其中的东西(symbol 或者 form)原封不动地放在那里,求值的过程被跳过了。

1 个赞

你的疑惑可能在于,为什么要发明 form 和 symbol 这两个概念。在我看来这是为了跟字符串区分开,evaluator 只处理抽象的对象(即 form 和 symbol)不关心具体的表示(字符串)。

reader 可以使用 reader macro 进行改造来处理中缀表达式,printer 可以被改造来按照你的需求打印。对此 evaluator 可以毫不知情,reader 交给它的依然是 form,它也不用关心 printer 的打印样式。

1 个赞

节选自我自译的一本书LISP,作者Partrick H. Winston

因为我觉得译得太烂所以没有公开发布。

现在是时候理解这些加入的单引号的含义了,我们将利用到 CAR 和 CDR 操作可以像算术函数一样被嵌套这一事实。为了得到一个列表的第二个元素,我们先使用 CDR,然后再使用CAR,因此,如果我们想得到 (A B C) 的第二个元素,看起来应该这么写:

(CAR (CDR (A B C)))

然而这里有个问题,我们想要 CDR 接收 (A B C),传回 (B C),这样 CAR 一定能返 回原来列表的第二个元素 B。但是 LISP 怎么知道哪里是要做什么的声明的结束,哪里 是要用来演算的数据的开始呢?观察这个嵌入的列表:

(A B C)

LISP 可能会合理地认为 A 是某个也许是由用户定义的函数。相似地,下面的 s 表达式 也自然是一个列表:

(CDR (A B C))

的确,它的第一个元素是 CDR! 因此下面的表达式得到的结果会是 CDR:

(CAR (CDR (A B C)))

对一个 s 表达式的求值过程应该做到何时停止呢?在这个问题的决断上,LISP 需要帮助。 用户以单引号字符的形式提供一个停止求值的标志信号,指明在哪里结束求值。这样下面的 式子可以返回 B:

(CAR (CDR '(A B C)))

返回的是 B,因为单引号标记阻止了 lisp 进入 (A B C) 并把它当作一个以 A 为作用于 B 和 C 的函数的 s 表达式,从而,(A B C) 被作为参数输入给 CDR,它接下来将 (B C) 交给 CAR,最终结果显然是 B。

移动单引号会导致结果的改变,如果我们输入 (CAR '(CDR (A B C))),那么 LISP 就 不会把 CDR 当作特殊的东西,而是简单地将 s 表达式 (CDR (A B C)) 作为一个列表 交给 CAR 处置,结果是 CDR,因为 CDR 是第一个元素。

忘记加单引号将会导致LISP尝试把 A 作为函数,而LISP 没有提供叫这个名字的函数,如 果用户也没有定义这样的函数,LISP 将会报告一个所谓的 undefined function(未定义 的函数)错误。

• 请记好,单引号机制的作用域是紧跟着它的那个 s 表达式。列表前的单引号会阻 止任何对该列表进行求值的尝试。

有时候,另一个停止求值的更古老的方法的是很有用的,s 表达式可以通过在左边添上 左括号和原子 QUOTE,右边补上相应右括号的方式保护起来,这样就能停止对列表 (A B C) 的求值。

EVAL流程图

2 个赞

我看完博文之后的理解是这样的:

  1. 程序就是一个从 characters → characters 的过程, 前一个 characters 是 source code, 后边一个是程序的输出, 我们暂时认为这就是一个程序(这里的程序输出值得是程序产生的任何人类或机器可读的信息表示, 而不是专指打印一个“hello world” 之类, 虽然这也是一种输出).
  2. REPL看成是一个loop套在characters → characters这个过程上. 这个过程包括3个阶段分别代表是 Read、Eval、Print.
  3. 总体的看这3个阶段, Read 是一个 接受 characters, 然后根据characters 构建计算机内部的objects/data structure, Eval 是一个 接受 (前一个阶段构建好的) objects 然后返回的也是objects , 这里是大部分有趣事情发生的地方. 最后是Print, 基本上是 Read 的逆过程, 接受 Eval 的 object 打印出 characters. 整体过程是: characters → objects → objects → characters, 三个箭头三个阶段.
  4. Read
    1. 遇到字符串就构造字符串, 遇到数字构造数字(这两种情况下基本有硬件的直接支持),
    2. 遇到list就构造list(也许就是个单链表),
    3. 遇到 ‘foo 也就是(quote foo) 就构造一个cons(也许就是单链表一个节点), car 指向 quote, cdr指向foo. 这里的 quote 和 foo 都是符号, 而符号可以看成是有五个字段name、function、value、property-list、package 的结构, 当然是一种object
    4. 最有趣的是Read遇到symbol比如 foo 的时候, 他会检查当前package里是不是已经存在一个name字段为 foo 的symbol. 如果存在就返回那个object, 如果不存在 就会构造一个新的 foo 对象(我已经说过它的类型为符号, 在计算机内部就是5个字段)
  5. Eval
    1. 对 number、string object直接返回(也就是给了什么object, Eval 仍然返回那个object),
    2. 对待一般的 symbol, Eval 返回symbol 中的 value, 但是对待 nil 和 t 这俩个symbol 也是直接返回.
    3. 对待一般的 list, Eval 把第一个元素取function, 递归对list 中的其它元素分别eval, 然后把 eval参数后得到的object给到function, 比如(+ 1 2) 对 + 这个符号取function, 分别对 1 2 eval 得到 1 2 两个objects, 然后apply.
    4. 重要的一点是25 个special operator的存在, 也就是Eval 硬编码了对 第一个元素是special operator 的 list的处理过程. 比如 quote, 如果 list 的第一个元素是quote的话, Eval 不会像对一般的list那样取quote这个符号的function 然后 Eval 参数, 而是直接返回这个 list 中 quote 后边那个元素(一个object)
  6. 以一个简单例子结束: lisp 怎么 处理我的程序 (print 'a)?
    1. read 在计算机内部构造一个list, 里面两个元素, 一个是符号print, 一个是cons (quote a), 其中print 是package中已经存在的, 而符号a是read帮我们创建的
    2. eval 接收read创建的object, 发现是一个 list, 取符号print的function, 再eval ’a
    3. eval 接收 (quote a) 这个object, 发现第一个元素是 quote, 直接返回符号 a 这个object
    4. apply function print to object symbol a, 对于一个symbol, print打印出 symbol 的 name, 也就是打印出 a, 同时 print 函数返回 它打印的object 也就是 符号 a
    5. REPL 中的 Print 打印出 print函数返回的 object a, 打印出a, 所以最后我们看到打印出两个a
(defun eval (thing)
  (cond
    ((numberp thing) thing)
    ((stringp thing) thing)
    ((symbolp thing) (symbol-value thing))
    ((listp thing)
     (destructuring-bind (head . arguments) thing
       (cond
         ((eq head (quote if))
          (destructuring-bind (test then else) arguments
            (if (eval test)
              (eval then)
              (eval else))))
         ((eq head (quote quote)) ; WHAT IN THE
          (first arguments))
         (t
          (apply (symbol-function head)
                 (mapcar #'eval arguments))))))
    ; ...
    (t (error "What the hell is this?"))))

我的理解是,实际上字面量不是必要的。LISP做的是符号演算。在read处理的时候也不一定要先转译成对象,对LISP来说它们也都是符号,一个字符串字面量也可以通过符号输入,例如:

"(THIS IS A STRING LITERAL“ 
=> (symbol-to-string |(THIS IS A STRING LITERAL)|)

换句话说,字面量可以视为一种特殊的符号。上面的symbol-to-stringeval才会求值,而字面量的版本在你的read里被直接求值。这并不影响你后面操作各种object,因为它们只是同一种数据不同的表示方法。

在LISP的上下文中,符号总是绑定到一个值或者一个列表,CL的书会说这个值就是一个对象,更老一点的书会说它是列表,符号或者字面量,因为他们可能用C一类的语言写过LISP的原型机。这取决于你打算采用什么表示来处理实现你的REPL所遇到的所有问题。对LISP来说eval只是一个函数,它把一个s表达式映射到另一个s表达式。

quote是为了控制eval的求值次序引入的一个东西,它与任意符号的组合都是eval的不动点。它与eval所处理的对象的表示没有关系,因为它定义在LISP语法层面。

(eval a)         ; => object bound to a
(eval (quote a)) ; => a

好像你更多的从语言的层面出发, 我提到的那些浅显的东西好像更是从实现的角度出发. 我发现对我个人而言, 从实现的角度看更好理解, 让我更有信心知道底层硬件在发生些什么.

(symbol-to-string |(THIS IS A STRING LITERAL)|)

这个东西事实上按我理解在eval前已经变成内存中的object了, 只不过不是一个string, 而是一个两元素的list, 一个是符号symboll-to-string 另一个 是一个我还没见过的东西 |(THIS IS A STRING LITERAL)| , 但是我知道这个东西一定已经变成内存中的object了, 因为我知道硬件一定是在操纵某些字节的. 显然 eval 对这个大的list object作用后给出了一个你想要的string object.

(eval a)         ; => object bound to a
(eval (quote a)) ; => a

这里面你好像更多的把 a 看成一个无实体的东西了, 只是一个头脑中虚构的绑定到这个value或者那个value的东西, 我发现我很难理解这种东西, 而 把symbol 描述成一个5字段的结构让我觉得满足. 上文中所有提到的object只是内存中的字节, 不过是有秩序的字节, 也就是data structure.

(eval a)         ; => 只是取出 a 的 value 字段中的 object
(eval (quote a)) ; => 返回 符号 a, 也就是一个类似5字段结构体的 object

symbol 和 quote 具体是什么… quote 是引用嘛,’ 也是引号,和字符串的 " 一样,都是告诉程序:「这里的这个不是代码,是数据。」而 quote 不仅能引用一段连续的字、数、符,告诉程序这是名为「符号」的数据,还能引用括号包裹的数据,告诉程序这是名为「列表」的数据,甚至还可以引用一个数组,引用一个哈希表。是的,很多 lisp 是可以用数组和哈希表作为语法的,最典型的就是字符串其实就是一个数组,当然,即使不作为语法 quote 也有这样引用让程序知道代码里的这样一段数据其实是引用。

至于 symbol 是什么… 我觉得我已经没有太多可以解释的了~ 唯一要提醒的是,symbol 也可以通过这种方式创建:(intern "A-SYMBOL10086"),这让运行环境中创建…或者说往一个存储 symbol 的 symbol table 中塞入 (intern) 一个形如 'a-symbol10086 的符号

另外我要回复一下这条的第一点,程序大多是一个「输入→计算→输出」的过程,输入和输出不一定是字符串。

题外话:实际上,现代计算机中的程序都有输入和输出。换种说法,现代计算机中的程序都有来自外部的副作用和影响外部的副作用。这是现代操作系统底层设计的缘故,就不展开说了。

我不确定我的感觉对不对, 你对symbol 和 quote的描述有一种 duck type 的感觉, 好像并没有很清楚这两个是什么, 推荐你可以看一下这个博文.

事实上, 我的本意是想说 lisp 对 lisp 程序的处理是从 characters → characters 的过程, 而不是描述 lisp 程序本身的 输入→计算→输出. 我发现我在上边把这二者混在一起了.

歪个楼,看到这个帖子在讨论 quoted symbol 和会求值的 symbol 的区别,其实 lisp 的 quoted symbol 和会求值的 symbol 的语义是很清晰的(不考虑宏)。

我想吐槽一下R的语义特别模糊(R 本质上也是一个 lisp),它也有会求值的 symbol 和 quoted 的 symbol 的区别,但是 quoted symbol 是不需要 quote 的,也就是一个 symbol 到底会求值后传给调用它的函数还是代表的只是一个 quoted symbol 是完全根据这个函数的内部实现决定的,在函数调用者这里没有任何区别,而且 R 没有宏,都是函数。

比如 filter(data, col > 1) 在这里 col 就是一个 quoted symbol,但是不需要调用者去标明这是一个 quote。filter 函数在内部实现会把 col 视做一个 quoted symbol 并去查找 data 里有没有名字叫 col 的列。但是假设有另一个函数 subset, 它的功能和 filter 完全一样,但是它不接受 quoted symbol,只接受值,调用者写出完全一样的代码 subset(data, col > 1) 却会报错,因为 col 是一个没有绑定到任何值的 void symbol。

我自然是读过那篇博文才能回答你的。我不太知道你说的 duck type 是指什么… 这里这个词的含义「不动像鸭子跑起来也像鸭子做什么都像鸭子就算不是鸭子也可以认为他是鸭子」这个意思对吧?那我再从另外一个角度回复吧。

首先,你已经理解的当然是对的,eval 中 quote 部分实现确实是:

(when (eq (first code) 'quote)
  (second code))

有言道「lisp 的代码也是数据」,就是说 lisp 的代码也是列表和 atom 这些数据构成的,那如何区分代码和数据嘛!quote 的作用也就很明显了,没有 quote 的当作代码一条条得解释,有 quote 的是数据在运行时交给代码运算。所以说 quote 和字符串一样是引用数据的,在 python 里 eval 的也是字符串是可以做到 eval("1 + 1") 这样的运算,就好像字符串符号 " 在代码里的作用是区分文本中的代码和字符串数据,quote 的作用是区分列表中的代码和数据。

就是lisp这个东西太早了,语法上不区分function call和 list literal,结果他发明个单引号来区分。

自己尝试写个解释器,就自然而然知道 quote 和 symbol 是啥了,或者说它们在底层究竟是如何实现的。并不是什么需要长篇大论讲述的概念。

1 个赞

实际上你并不知道内存中发生了什么,object也只是一种表示形式,字节也只是一种表示形式,即使是你的程序,它操作的也根本不是它想象中的那一块内存地址区域。你觉得最底层的理解可能也只是表面的,问题不在于什么是更基本的,而在于我们希望用什么基本对象表示当前的问题,这样我们就可以处理它了。如果你在采用CL实现一个简单的REPL,那么你处理的基础对象就是object。

这里的例子并不是把符号视为无实体的东西,相反,它可以是任意的实体。比如说,假如我们是用C实现一个带内存管理的LISP系统,你也许会设置一个符号段,在这个符号段里储存了所有符号对应的字符串。如果一个列表结点的CAR落在这个内存段中,那你就知道它显然是一个符号。或者,你也可以设置一个哈希表,每个hash id对应一个符号的key,这样你在处理符号时会有更高的查询效率。这对底层实现来说都是可行的。你可以把这个符号段一起放在所有类型的值的内存段里,也可以分开,也可以把储存所有值的段就叫做符号段。数据在底层的表示方法取决于

  1. 你当前使用的环境支持创建什么样的抽象。
  2. 在待解决的实际问题中它们会被如何处理。

我之前说的不需要字面量的例子是说,你可以只用符号完成这一切。字面量对应的那些对象,对于外部负责底层实现的LISP系统(比如这里的CL)而言,本质上也都是抽象对象,不需要与你实现的LISP中是一致的表示。