Lisp源代码文件本身是列表吗?

时间:2012-05-09 16:47:40

标签: emacs clojure lisp scheme common-lisp

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

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

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

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

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

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

我有点惊讶这整个“代码就是数据,数据是代码”看东西看起来源代码文件显然不是整齐的列表...或者是他们!?

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

如果我想将我的 .clj (Clojure)源文件中的一个转换为某些CSS + HTML网页,那么源代码文件显然不是“问题”怎么办?本身不是一个清单?

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

7 个答案:

答案 0 :(得分:11)

在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系统允许写一个'图像'当前执行内存 - 此图像包括所有数据和所有代码(也包括已编译的代码)。因此,您可以处理此数据/代码并不时保存图像。

答案 1 :(得分:10)

源代码文件只是存储列表的便利位置。 Lisp代码(通常)旨在以read-eval-print-loop(REPL)执行,其中每个输入本身就是一个列表。因此,当您执行源代码文件时,您可以想到它,因为它中的每个列表都被逐个读入REPL。我们的想法是,您拥有一个完全互动的环境,可以称赞“代码就是数据”范例。

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

答案 2 :(得分:6)

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

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

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

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

答案 3 :(得分:5)

在Lisp中有两级源代码,或根本没有源代码,具体取决于您如何定义源代码。

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

第一步:“阅读”

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

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

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

因此,在这种观点中,你的答案的问题是肯定而不是。对于步骤1是否,对于步骤2是。如果您仅考虑文件,则答案为否...文件包含字符,而不是列表。读者可以将这些字符转换为列表。

Lisp没有语法

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

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

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

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

但这不是一个严格的约束......例如,我并不是在开玩笑地将Lisp语法变形为Python:someone did exactly that

答案 4 :(得分: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列表式语法,更不用说作为一个巨型列表。它可以自由地处理源文本。

答案 5 :(得分:2)

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

基本理念是这样的:

编译器(或解释器)通过您的输入(REPL或源文件)。然后将每个列表评估作为“表单”。大多数表单(列表)将类似于defun。评估defun表单会导致符号表发生更改(这是另一个讨论的主题) - 它def根据表单中的符号名称开始fun ction。 ((defun foo (bar) (print bar))定义符号表应该有foo的条目,有效地评估为(lamba (bar) (print bar))。)

这些列表引用,因为我们希望立即评估它们。引用'(…)(quote …)意味着阻止编译器/ REPL立即评估某些内容。

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

如果您提供的内容如下:

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

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

尽管如此,你可以使用read而不是 eval来读取Lisp源文件中的内容,完全按照说:生成HTML“副本”或类似内容。

一旦您深入研究funcalldefmacro,了解所有这些引用所属的位置(甚至更好的是,反引号逗号引用 - 取消引用范例)可能需要一段时间才能习惯...

答案 6 :(得分:0)

在Lisp中,您直接编程为抽象语法树,表示为嵌套列表。由于Lisp是以其自己的列表数据结构表示的,因此宏会因此而失去代理,因为这些列表可以以编程方式进行修改。我想最顶级的列表是隐含的,这就是为什么,至少在Clojure中,你看不到程序以(开始和结束...... )