无论Lisp方言如何,看起来每个包含Lisp函数的源代码文件本身都不是一个列表(我第一次对此感到“惊讶”是在处理我的Emacs .el 文件)。
我有几个问题,但它们都与同一个“问题”有关,而且可能只是我误解了一些问题。
为什么有各种各样的Lisp方言的源代码文件似乎是一堆像这样的“混乱”函数:
(function1 ...)
(function2 ...)
(function3 ...)
而不是函数的“Lisp列表”,可能是这样的:
(
'(function1 ...)
'(function2 ...)
'(function3 ...)
)
我有点惊讶这整个“代码就是数据,数据是代码”看东西看起来源代码文件显然不是整齐的列表...或者是他们!?
源代码文件是否应该“操纵”?
如果我想将我的 .clj (Clojure)源文件中的一个转换为某些CSS + HTML网页,那么源代码文件显然不是“问题”怎么办?本身不是一个清单?
我从Lisp开始,所以我不知道我的问题是否有意义,任何解释都是受欢迎的。
答案 0 :(得分:11)
在Common Lisp中,源文件包含lisp forms
和注释。 Lisp表单是数据或Lisp代码。源文件的典型操作由函数LOAD
和COMPILE-FILE
完成。
LOAD
将从文件中读取表单并逐个执行。
COMPILE-FILE
要复杂得多。它通常读取表单并将它们编译为其他表示形式(机器代码,字节代码,C代码......)。它不执行代码。
如果文件包含一个表单列表而不是仅包含多个表单,那对你有什么帮助?
现在举个例子,编译器会从文件流中读取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,前两个字母代表READ
和EVAL
。 READ
采用代码的字符串表示形式,并返回表示代码的数据结构。 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没有语法,实际上有两个不同的语法级别?原因是这两个级别都在程序员的控制之下。
您可以通过定义阅读器宏来自定义级别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“副本”或类似内容。
一旦您深入研究funcall
和defmacro
,了解所有这些引用所属的位置(甚至更好的是,反引号逗号引用 - 取消引用范例)可能需要一段时间才能习惯...
答案 6 :(得分:0)
在Lisp中,您直接编程为抽象语法树,表示为嵌套列表。由于Lisp是以其自己的列表数据结构表示的,因此宏会因此而失去代理,因为这些列表可以以编程方式进行修改。我想最顶级的列表是隐含的,这就是为什么,至少在Clojure中,你看不到程序以(
开始和结束...... )
。