简介: Clojure 是 EDN 的超集。默认情况下,当pr
给定 Clojure 数据结构时,会生成有效的 EDN。改变了这一点,并使它们利用 Clojure 的全部功能在往返后对内存中对象的“相同性”提供更强的保证。ClojureScript 只能读取 EDN,不能读取完整的 Clojure。prn
pr-str
*print-dup*
简单的解决方案:不要设置*print-dup*
为 true,只将纯数据从 Clojure 传递到 ClojureScript。
更难的解决方案:使用标记文字,两边都有一个(可能共享的)关联阅读器。(不过,这仍然不涉及*print-dup*
。)
切线相关:EDN 的大多数用例都包含在Transit中,它更快,尤其是在 ClojureScript 方面。
让我们从 Clojure 部分开始。Clojure 从一开始就有一个clojure.core/read-string
函数,它read
是旧 Lispy 意义上的 Read-Eval-Print-Loop 中的字符串,即它可以访问在 Clojure 编译中使用的实际阅读器。 [0]
后来,Rich Hickey & co 决定推广 Clojure 的数据表示法,并发布了EDN 规范。EDN 是 Clojure 的一个子集;它仅限于 Clojure 语言的数据元素。
由于 Clojure 是一个 Lisp,并且与所有 lisp 一样,吹捧“代码就是数据就是代码”的理念,因此上述段落的实际含义可能并不完全清楚。我不确定任何地方都有详细的差异,但仔细检查Clojure Reader 描述和前面提到的 EDN 规范会发现一些差异。最明显的区别在于宏字符,尤其是#
调度符号,它在 Clojure 中的目标比在 EDN 中多得多。例如,#(* % %)
表示法是有效的 Clojure,Clojure 阅读器将把它变成以下 EDN 的等价物:(fn [x] (* x x))
. 对这个问题特别重要的是很少记录的#=
特殊阅读器宏,它可以用来在阅读器内部执行任意代码。
由于 Clojure 阅读器可以使用完整的语言,因此可以将代码嵌入到阅读器正在阅读的字符串中,并在阅读器中立即对其进行评估。可以在此处找到一些示例。
该clojure.edn/read-string
功能严格限于 EDN 格式,而不是整个 Clojure 语言。特别是,它的操作不受*read-eval*
变量的影响,它无法读取所有可能的有效 Clojure 代码片段。
事实证明,由于历史原因,Clojure 阅读器是用 Java 编写的。由于它是一个重要的软件,运行良好,并且经过几年在野外活跃使用 Clojure 的大量调试和实战测试,Rich Hickey 决定在 ClojureScript 编译器中重用它(这是为什么ClojureScript 编译器在 JVM 上运行)。ClojureScript 编译过程主要发生在 JVM 上,那里有 Clojure 阅读器,因此 ClojureScript 代码由clojure.core/read-string
(或者更确切地说是它的近亲clojure.core/read
)函数解析。
但是您的 Web 应用程序无法访问正在运行的 JVM。ClojureScript 应用程序需要 Java 小程序看起来不是一个很有前途的想法,尤其是 ClojureScript 的主要目标是将 Clojure 语言的范围扩展到 JVM(和 CLR)之外。所以决定 ClojureScript 不能访问它自己的阅读器,因此也不能访问它自己的编译器(即在 ClojureScript中没有eval
nor read
nor )。read-string
这个决定及其影响在这里有更详细的讨论,由真正知道事情是如何发生的人(我不在场,所以这个解释的历史观点可能存在一些不准确之处)。
所以 ClojureScript 没有等价物clojure.core/read-string
(有些人会争辩说它因此不是真正的 lisp)。尽管如此,在 Clojure 服务器和 ClojureScript 客户端之间有某种方式来通信 Clojure 数据结构还是不错的,这确实是 EDN 工作的激励因素之一。正如在 EDN 规范发布后Clojure 获得了一个受限(且更安全)的阅读功能(有人可能会争辩说,这两个函数的名称(或者更确切地说是它们的命名空间)之间的一致性会更好。clojure.edn/read-string
cljs.reader/read-string
在我们最终回答您最初的问题之前,我们还需要一点关于*print-dup*
. 请记住,这*print-dup*
是 Clojure 1.0 的一部分,这意味着它早于 EDN、标记文字的概念和记录。我认为 EDN 和标记文字为*print-dup*
. 由于 Clojure 通常建立在一些数据抽象(列表、向量、集合、映射和通常的标量)之上,因此打印/读取循环的默认行为是保留数据的抽象形状(映射是map),但不是特别是它的具体类型。例如,Clojure 有多个 map 抽象的实现,例如用于小地图的PersistentArrayMap和PersistentHashMap对于更大的。该语言的默认行为假定您不关心具体类型。
对于您这样做的极少数情况,或者对于更专业的类型(当时使用 deftype 或 defstruct 定义),您可能需要更多地控制它们的读取方式,这就是 print-dup 的用途。
关键是,*print-dup*
设置为true
和pr
family 不会产生有效的 EDN,但实际上 Clojure 数据包括一些显式#=(eval build-my-special-type)
形式,它们不是有效的 EDN。
[0]:在“lisps”中,编译器是根据数据结构明确定义的,而不是根据字符串。虽然这与通常的编译器(在处理过程中确实将字符流转换为数据结构)看起来似乎有细微差别,但 Lisp 的定义特征是阅读器发出的数据结构是通常使用的数据结构语言。换句话说,编译器基本上只是该语言中始终可用的函数。这不像以前那样独特,因为大多数动态语言都支持某种形式的eval
; Lisp 的独特之处在于eval
采用数据结构,而不是字符串,这使得动态代码生成和评估更加容易。编译器“只是另一个功能”的一个重要含义是编译器实际上运行时使用了已经定义和可用的整个语言,并且到目前为止读取的所有代码也可用,这为 Lisp 宏系统打开了大门。