用比尔克林顿的话来说,“这取决于‘是’这个词的含义是什么”。好吧,好吧,不是真的,但这确实取决于“同音”这个词的含义。这个词有足够的争议,我们不再说 Julia 是谐音的——所以你可以自己决定它是否符合条件。与其试图定义同音性,我将引用Kent Pitman(他对 Lisp 略知一二)在 2001 年的Slashdot 采访中所说的话:
我喜欢 Lisp 愿意代表自己。人们经常将此解释为它代表自己的能力,但我认为这是错误的。大多数语言都能够代表自己,但他们根本没有意愿。Lisp 程序由列表表示,程序员知道这一点。如果它是数组也没关系。重要的是表示的是程序结构,而不是字符语法,但除此之外,选择是相当随意的。代表是正确的选择并不重要。重要的是,它是一个共同的、商定的选择,这样才能有一个丰富的程序操纵程序社区,在这个共同的表示中“进行交易”。
他也没有定义同音性——他可能比我更不想进入定义论证。但他切入了问题的核心:一种语言有多愿意代表自己?Lisp 非常愿意——你甚至无法避免它:程序作为数据的表示只是坐在那里,盯着你的脸。Julia 不使用 S 表达式语法,因此代码作为数据的表示不太明显,但它并没有隐藏得很深:
julia> ex = :(2a + b + 1)
:(2a + b + 1)
julia> dump(ex)
Expr
head: Symbol call
args: Array(Any,(4,))
1: Symbol +
2: Expr
head: Symbol call
args: Array(Any,(3,))
1: Symbol *
2: Int64 2
3: Symbol a
typ: Any
3: Symbol b
4: Int64 1
typ: Any
julia> Meta.show_sexpr(ex)
(:call, :+, (:call, :*, 2, :a), :b, 1)
julia> ex.args[3]
:b
julia> ex.args[3] = :(3b)
:(3b)
julia> ex
:(2a + 3b + 1)
Julia 代码由Expr
类型(以及符号和原子)表示,虽然表面语法和结构之间的对应关系不太明显,但它仍然存在。更重要的是,人们知道代码只是可以生成和操作的数据,因此正如 KMP 所说,有一个“丰富的程序操作程序社区”。
这不仅仅是 Julia 代码作为数据结构的肤浅呈现——这就是 Julia 将其代码呈现给自己的方式。当您在 REPL 中输入表达式时,它会被解析为Expr
对象。然后将这些Expr
对象传递给eval
,这会将它们“降低”为更常规Expr
的对象,然后将其传递给类型推断,所有这些都在 Julia 中实现. 关键是编译器使用与您看到的完全相同的代码表示。Lisp 中的情况并没有什么不同。当您查看 Lisp 代码时,您实际上并没有看到列表对象——它们只存在于计算机的内存中。您看到的是列表文字的文本表示,Lisp 解释器将其解析并转换为列表对象,然后进行评估,就像 Julia 一样。Julia 的语法可以看作是文字的文本表示Expr
——它Expr
恰好是一种比列表更不通用的数据结构。
我不知道细节,但我怀疑 Elixir 是相似的——也许 José 会插话。
更新 (2019)
在过去 4 年多的时间里,我更多地考虑了这一点,我认为 Lisp 和 Julia 之间的主要区别在于:
- 在 Lisp 中,代码的语法与用于表示该代码的数据结构的语法相同。
- 在 Julia 中,代码的语法与表示该代码的数据结构的语法完全不同。
为什么这很重要?在支持 Julia 的一方,人们喜欢事物的特殊语法,并且经常发现 S 表达式语法不方便或令人不快。在支持 Lisp 方面,当您尝试生成的数据结构的语法(表示代码)与您通常编写的代码的语法相同时,更容易弄清楚如何正确进行元编程. 这就是为什么当人们尝试在 Julia 中编写宏时,最好的建议之一是执行以下操作:
- 编写一个您希望宏生成的代码类型的示例
- 调用
Meta.@dump
该代码以将其视为数据结构
- 编写代码来生成该数据结构——这是你的宏。
在 Lisp 中,您不必执行第 2 步,因为代码的语法已经与数据结构的语法相同。Julia中有准引用(在 Lisp 中)quote ... end
和:(...)
构造,它们允许您使用代码语法构造数据结构,但这仍然不如让它们首先使用相同的语法那么直接。
也可以看看: