在什么意义上像 Elixir 和 Julia 这样的语言是谐音的?

In what sense are languages like Elixir and Julia homoiconic?

Lisp 中的同象性很容易看出:

(+ 1 2)

既是以12为参数对+的函数调用,也是包含+1的列表,和 2。它同时是代码和数据。

虽然在像 Julia 这样的语言中:

1 + 2

我知道我们可以在 Julia 中将其解析为 Expr

:(1 + 2)

然后我们可以获取 AST 并对其进行操作:

julia> Meta.show_sexpr(:(1+2)) (:call, :+, 1, 2)

因此,我们可以在 Julia(和 Elixir)中操作程序的 AST。但它们是否与 Lisp 具有相同的意义?任何 代码片段真的只是语言本身的数据结构吗?

我不明白像 Julia 中的 1 + 2 这样的代码是如何立即成为数据的,就像 Lisp 中的 (+ 1 2) 只是一个列表一样。那还是谐音吗?

用比尔·克林顿的话来说,"It depends upon what the meaning of the word 'is' is"。好吧,不是真的,但这确实取决于 "homoiconic" 这个词的含义。这个术语有足够的争议,我们不再说 Julia 是谐音的——所以你可以自己决定它是否符合条件。与其试图定义同形性,不如引用 2001 年 Kent Pitman (who knows a thing or two about Lisp) said in a Slashdot interview 的话:

I like Lisp's willingness to represent itself. People often explain this as its ability to represent itself, but I think that's wrong. Most languages are capable of representing themselves, but they simply don't have the will to. Lisp programs are represented by lists and programmers are aware of that. It wouldn't matter if it had been arrays. It does matter that it's program structure that is represented, and not character syntax, but beyond that the choice is pretty arbitrary. It's not important that the representation be the Right® choice. It's just important that it be a common, agreed-upon choice so that there can be a rich community of program-manipulating programs that "do trade" in this common representation.

他也没有定义同形性——他可能和我一样不想陷入定义争论。但他切入了问题的核心:一种语言有多愿意代表自己? Lisp 在极端情况下愿意——你甚至无法避免它:程序作为数据的表示就坐在那里,盯着你的脸。 Julia 不使用 S-expression 语法,因此代码作为数据的表示不太明显,但隐藏得不是很深:

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 类型(以及符号和原子)表示,虽然表面语法和结构之间的对应关系不是那么明显,但它仍然存在。更重要的是,人们知道代码只是可以生成和操作的数据,所以有 "rich community of program-manipulating programs",正如 KMP 所说。

这不仅仅是将 Julia 代码作为一种数据结构进行表面呈现——这就是 Julia 将其代码呈现给自己的方式。当您在 REPL 中输入表达式时,它会被解析为 Expr 个对象。然后将那些 Expr 对象传递给 eval,然后将它们 "lowers" 传递给更规则的 Expr 对象,然后传递给类型推断,全部实现 in Julia .关键是编译器使用与您看到的完全相同的代码表示。在 Lisp 中情况并没有那么不同。当你查看 Lisp 代码时,你实际上并没有看到列表对象——它们只存在于计算机的内存中。你看到的是列表文字的文本表示,Lisp 解释器解析它并将其转换为列表对象,然后对其求值,就像 Julia 一样。 Julia 的语法可以看作是 Expr 文字的文本表示 - Expr 恰好是一种比列表更不通用的数据结构。

我不知道细节,但我怀疑 Elixir 是相似的——也许 José 会插话。

更新 (2019)

在过去 4 年多的时间里对此进行了更多思考,我认为 Lisp 和 Julia 之间的主要区别在于:

  • 在 Lisp 中,代码的语法与用于表示该代码的数据结构的语法相同。
  • 在 Julia 中,代码的语法与表示该代码的数据结构的语法完全不同。

为什么这很重要?在支持 Julia 的一方,人们喜欢事物的特殊语法,并且经常发现 S 表达式语法不方便或令人不快。在亲 Lisp 方面,当您尝试生成(表示代码)的数据结构的语法与您通常编写的代码的语法相同时,更容易弄清楚如何正确地进行元编程.这就是为什么当人们试图在 Julia 中编写宏时最好的建议之一是执行以下操作:

  1. 编写您希望宏生成的代码类型的示例
  2. 对该代码调用 Meta.@dump 以将其视为数据结构
  3. 编写代码来生成该数据结构——这是您的宏。

在 Lisp 中,您不必执行第 2 步,因为代码的语法已经与数据结构的语法相同。 Julia 中有准引用(用 Lisp 语言)quote ... end:(...) 结构,它们允许您使用代码语法构造数据结构,但这仍然不如让它们在第一名。

另请参阅: