9

无论 Lisp 方言如何,看起来每个包含 Lisp 函数的源代码文件本身都不是一个列表(我第一次对此感到“惊讶”是在处理我的 Emacs .el文件时)。

我有几个问题,但它们都与同一个“问题”有关,可能只是我误解了一些事情。

为什么各种 Lisp 方言的源代码文件似乎是一堆“杂乱无章”的函数,像这样:

(function1 ...)
(function2 ...)
(function3 ...)

而不是函数的“Lisp 列表”,可能是这样的:

(
  '(function1 ...)
  '(function2 ...)
  '(function3 ...)
)

我对整个“代码就是数据,数据就是代码”的事情感到有点惊讶,因为看到源代码文件本身显然不是整洁的列表......或者他们是!?

源代码文件是否应该“操纵”?

例如,如果我想将我的一个.clj (Clojure) 源文件转换为某个 CSS+HTML 网页怎么办,源代码文件本身显然不是一个列表,这难道不是一个“问题”吗?

我从 Lisp 开始,所以我不知道我的问题是否有意义,欢迎任何解释。

4

7 回答 7

13

在 Common Lisp 中,源文件包含lisp forms和注释。Lisp 表单是数据或 Lisp 代码。对源文件的典型操作由函数LOADCOMPILE-FILE.

LOAD将从文件中读取表单并一一执行。

COMPILE-FILE复杂得多。它通常读取表单并将它们编译为其他表示形式(机器代码、字节代码、C 代码……)。它不执行代码。

如果文件包含一个表单列表而不是多个表单,它将对您有什么帮助?

  • 你会有一层添加的括号
  • 您必须先阅读整个列表,然后才能对其进行任何操作(或者您需要不同的阅读器机制)
  • 通过程序将表单添加到文件的末尾会很痛苦
  • 您不能在文件中添加一些内容,这会改变读者对文件其余部分的解释
  • 对于 LOAD,文件不能无限长

现在举个例子,编译器将从文件流中读取 lisp 形式并逐段编译它们。

如果你想要所有表格,你可以做

CL-USER 170 > (defun read-forms (file)
               (with-open-file (stream file)
                 (loop for form = (read stream nil nil)
                       while form
                       collect form)))
READ-FORMS

CL-USER 171 > (read-forms (capi:prompt-for-file "source file"))
((DEFPARAMETER *UNITS-TO-SHOW* 4.1)
 (DEFPARAMETER *TEXT-WIDTH-IN-PICAS* 28.0)
 (DEFPARAMETER *DEVICE-PIXELS-PER-INCH* 300)
 (DEFPARAMETER *PIXELS-PER-UNIT* (* (/ (/ *TEXT-WIDTH-IN-PICAS* 6)
                                       (* *UNITS-TO-SHOW* 2))
                                    *DEVICE-PIXELS-PER-INCH*))
...

如果您想在所有内容周围加上括号,请使用PROGN

 (progn
   'form-1
   (defun function-defintion-form () )
   42)

PROGN还保留了其子形式的“顶级”。

旁注:几十年来,Lisp 已经探索了替代方案。最突出的例子是施乐公司现已不复存在的 Interlisp-D。Interlisp-D 由 Xerox PARC 与 Smalltalk 并行开发。Interlisp-D 最初使用结构编辑器来编辑 Lisp 数据,源代码被编辑为这样的 Lisp 数据。开发环境就是基于这个想法。但从长远来看,“作为文本的来源”赢了。您仍然可以在许多当前的 Lisp 环境中模拟其中的一些。例如,许多 Lisp 系统允许编写当前执行内存的“图像”——该图像包括所有数据和所有代码(也包括编译后的代码)。因此,您可以处理此数据/代码并不时保存图像。

于 2012-05-09T19:34:30.087 回答
10

源代码文件只是一个方便的地方来存储您的列表。Lisp 代码(通常)旨在在读取-评估-打印-循环(REPL)中执行,其中每个输入本身就是一个列表。所以当你执行一个源代码文件时,你可以把它想象成其中的每个列表都被一个一个地读入一个 REPL。这个想法是你有一个完全交互式的环境,它补充了“代码就是数据”范式。

当然,您可以将文件视为一个巨型列表,但是您暗示该文件具有明确定义的结构,但并非总是如此。如果你真的想创建一个包含一个巨大列表的文件,那么没有什么能阻止你这样做。您可以使用 Lisp 阅读器将其作为一个大列表(数据?)读取并根据需要处理它(可能使用某种 eval?)。以 Leiningen 的 project.clj 文件为例。它们通常只是一个大的 defproject 列表。

于 2012-05-09T16:54:14.963 回答
6

在 Lisp 中有两个级别的源代码,或者根本没有源代码,这取决于您如何定义源代码。

存在这两个级别是因为(通常)由 Lisp 解释器/编译器执行两个独立的概念步骤。

第一步:“阅读”

在这一步中,源代码是一个字符序列,例如来自一个文件。在这里,括号、带引号的字符串、数字、符号、引号甚至部分准引号语法都被处理并转换为 Lisp 数据结构。在这个级别,语法规则是关于括号、数字、管道、引号、分号、尖号、逗号、at 符号等。

第二步:“编译”/“解释”

在这一步中,输入是 Lisp 数据结构,输出是机器码、字节码,或者源代码可能直接由解释器执行。在这个级别,语法是关于特殊形式的含义......例如(if ...),,(labels ...)等等(symbol-macrolet ...)。Lisp 代码中的结构是统一的(只是列表和原子),但语义不是(if表单看起来像函数调用,但它们不是)。

所以在这个观点中,你的答案是肯定的和否定的。第 1 步否,第 2 步是。如果您只考虑文件,那么答案是否定的……文件包含字符,而不是列表。读者可以将这些字符转换为列表。

Lisp 没有语法

为什么有人说 Lisp 没有语法,而实际上有两个不同的语法级别?原因是这两个级别都在程序员的控制之下。

您可以通过定义阅读器宏来自定义级别 1,您可以通过定义宏来自定义级别 2。所以 Lisp 没有固定的语法,因此源文件可以以“lispy”的外观开始,并且可以看起来与 Python 代码完全一样。

源文件可以包含任何内容(从某一点开始),因为初始形式可以定义一些新的阅读规则,这些规则将改变后面字符的含义。

通常 Lisp 程序员不会在阅读级别上做疯狂的事情,因此大多数 Lisp 源代码文件看起来就像 Lisp 表单的序列,并且它们仍然是“lispy”。

但这不是一个硬性约束……例如,我并不是在开玩笑说 Lisp 语法变成了 Python:有人确实做到了

于 2012-05-10T06:50:29.257 回答
6

为了彻底,所有源文件都是文本,而不是 lisp 数据结构。要评估或编译代码,lisp 必须首先READ文件,这意味着将文本转换为 lisp 数据结构。回想一下首字母缩略词 REPL,前两个字母代表READEVALREAD接受代码的字符串表示,并返回表示代码的数据结构。EVAL获取返回的数据结构,并将数据结构解释(或编译并运行)为代码。因此,重要的是要记住涉及中间步骤。

一个很好的问题是,当多个 s 表达式被传递给READ并且它们不在列表中时会发生什么,正如您提到的那样?

如果您查看代码,您通常会发现READclojure 的多个版本read-string只读取并返回第一个 s 表达式,而忽略其余的。但是,clojure 中使用的阅读器load-file将获取整个字符串,并“有效地”(实现可能不同)围绕所有形式包装一个隐式do(或progn普通 lisp),然后将其传递给eval. 这种行为与 REPL 中发生的情况形成对比,表单是按顺序读取、评估和打印的。

不过,在这两种情况下,这种“幕后”行为都是为了简洁而做出的权衡。我们可以假设当我们加载一个 s 表达式的文本文件时,我们希望它们都被计算,并且最多返回最后一个 s 表达式的值。

于 2012-05-09T18:47:08.717 回答
3

(Lisp 的)开头有一个交互式 REPL读取,然后评估,然后打印结果并再次询问,循环。您将在提示符处键入一些文本。运行时系统将“读取”它,将文本转换为“代码”的内部表示,然后评估(“执行”或其他)

> (setq s "(setq a 2)")
"(setq a 2)"
> (type-of s)          ; s is just a bunch of text characters
(SIMPLE-BASE-STRING 10)
> (setq r (read (make-string-input-stream s)))
(SETQ A 2)
> (type-of r)          ; the result of reading is Lisp data - a CONS cell
CONS                   ;     - - - - - - - - -    ~~~~~~~~~
> (type-of 'a)         ; A is just a symol
SYMBOL
> (type-of a)          ; ERROR: A has no value    
*** - EVAL: variable A has no value

> (eval r)             ; now what? The data got treated as code.
2                      ;               ~~~~                ~~~~
> a                    ; 'A' has got its value
2
> (setf (caddr r) 4)   ; alter the Lisp data object! that is 
4                      ;  the value of a symbol 'r'
> (eval r)             ; execute the altered data, as 
4                      ;  new version of code
> a
4

所以你看,“s-expressions”、AST 等等都是抽象的,它们在 Lisp 中由具体的、简单的、基本的 Lisp 数据对象表示。

现在源文件并不神秘,它们只是让我们不必在 REPL 中一遍又一遍地键入我们的定义。如何读取源文件的内容完全是任意的,取决于具体的实现。您也可以轻松地拥有可以读取 Python、Haskell 或类似 C 的语法文件的实现。

当然,Common Lisp 标准定义了它的兼容实现应该如何读取它的 Common Lisp 源文件。但是您的系统也可以定义一些附加格式为有效读取。最重要的是,它受到将它们都表示为类似 Lisp 列表的语法的需要的限制,更不用说作为一个巨大的列表。可以随意处理源文本。

于 2012-05-14T09:15:54.227 回答
2

你建议的变体——有一个引用列表的列表——可能反映了(恕我直言)关于 Lisp ☺ 最令人困惑的事情是什么——引用!

基本的想法是这样的:

编译器(或解释器)传递您的输入(REPL 或源文件)。然后将每个列表评估为“表格”。大多数表单(列表)将属于defun. 评估defun表单会导致符号表发生变化(这是另一个讨论的主题)——它会根据表单中的符号名称执行操作deffun((defun foo (bar) (print bar))定义符号表应该有一个有效地foo计算为的条目(lamba (bar) (print bar))。)

这些列表没有被引用,因为我们希望它们立即被评估。'(…)用or引用(quote …)是为了防止编译器/REPL 立即评估某些东西。

您的编译器的输出(取决于它是哪一个)通常是某种二进制或字节码,其中包含您定义的所有这些函数;或者,也许只是最终被某种“主要功能”引用的那些。

如果您提供了类似的内容:

 (
         '(defun foo (bar) (print bar))
 )

您的编译器将尝试评估外部列表的第一个元素,这是一个带引号的defun特殊形式(或宏),并且没有任何事情可做。

尽管如此,您可以read使用而不是 它来执行诸如读取 Lisp 源文件之类的操作eval,就像您说的那样:生成 HTML“副本”或类似内容。

一旦深入研究funcalland defmacro,了解所有这些引号的归属(以及更好的是,反引号-逗号引用-取消引用范式)可能需要一段时间才能习惯......</p>

于 2012-05-09T17:16:10.883 回答
0

在 Lisp 中,您可以直接对抽象语法树进行编程,该语法树表示为嵌套列表。由于 Lisp 是根据它自己的列表数据结构来表达的,因此宏会因此而失效,因为这些列表可以通过编程方式进行修改。我想最顶层的列表是隐含的,这就是为什么,至少在 Clojure 中,你看不到以(...开头和结尾的程序)

于 2012-05-09T17:04:03.143 回答