Clojure和ClojureScript:clojure.core/read-string、clojure.edn/read-string和cljs.reader/read-string

27

我不清楚所有这些read-string函数之间的关系。显然,clojure.core/read-string 可以读取由 pr[n] 或甚至 print-dup 输出的任何序列化字符串。同时,clojure.edn/read-string 可以读取按照EDN规范格式化的字符串。

然而,我正在使用Clojure Script,而不清楚 cljs.reader/read-string 是否符合相同规范。这个问题是因为我有一个Web服务,它发出的是用Clojure代码序列化的方式:

(with-out-str (binding [*print-dup* true] (prn tags)))

那段代码生成了包含数据类型的对象序列化,但是cljs.reader/read-string无法读取。我一直都遇到了这种错误:

Could not find tag parser for = in ("inst" "uuid" "queue" "js")  Format should have been EDN (default)
起初我以为这个错误是由cljs-ajax引起的,但是在一个Rhino REPL中测试cljs.reader/read-string之后,我得到了同样的错误提示,这意味着它是由cljs.reader/read-string自身引发的。它是由cljs.reader中的maybe-read-tagged-type函数引发的,但目前还不清楚这是因为阅读器仅适用于EDN数据,还是因为...?

另外,在与Clojure的差异文档中,唯一提到的是:

The read and read-string functions are located in the cljs.reader namespace

这表明它们应该具有完全相同的行为。

3个回答

54
简介:Clojure是EDN的超集。默认情况下,当给定Clojure数据结构时,prprnpr-str会产生有效的EDN。 *print-dup*更改了这一点,并使它们使用Clojure的全部功能,以在往返后对内存中的对象的“相同性”提供更强的保证。ClojureScript只能读取EDN,而不能读取完整的Clojure。

简单的解决方案:不要将*print-dup*设置为true,并且只从Clojure传递纯数据到ClojureScript。

更复杂的解决方案:使用标记文字,其中包含(可能共享的)关联读取器。 (尽管如此,这仍不涉及*print-dup*。)

与此有关的次要问题:大多数EDN的用例都可以通过Transit来处理,它更快,特别是在ClojureScript方面。


让我们从Clojure部分开始。Clojure从一开始就有一个clojure.core/read-string函数,它以旧的Lispy意义上的读取-求值-打印循环(Read-Eval-Print-Loop)方式read字符串,即它提供了实际在编译Clojure时使用的读取器的访问权限。[0]

后来,Rich Hickey和他的团队决定推广Clojure的数据表示法,并发布了EDN规范。EDN是Clojure的一个子集,仅限于Clojure语言的数据元素。

作为 Lisp 方言的 Clojure,和所有的 Lisp 一样,它宣扬 "代码即数据,数据即代码" 的哲学。因此,上面这段话的实际含义可能并不完全清晰。我不确定是否有任何详细的差异说明,但是对Clojure Reader description 和之前提到的 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、read或read-string)。这个决定及其影响在这里进行了更详细的讨论,由一个实际知道事情经过的人进行(我不在那里,因此在这个解释的历史观点上可能存在一些不准确之处)。
因此,ClojureScript没有clojure.core/read-string的等效物(有些人会认为它因此不是真正的lisp)。尽管如此,在Clojure服务器和ClojureScript客户端之间通信Clojure数据结构仍然是很好的,这也是EDN计划的推动因素之一。就像在EDN规范发布后Clojure获得了受限制(更加安全)的读取函数(clojure.edn/read-string)一样,ClojureScript也在标准发行版中获得了一个EDN读取器,即cljs.reader/read-string。可以说,这两个函数(或者说它们的命名空间)之间的一致性需要更好一些。

在我们最终回答您的原始问题之前,我们需要了解更多关于*print-dup*的背景。请记住*print-dup*是Clojure 1.0的一部分,这意味着它先于EDN、tagged literals和records。我认为EDN和tagged literals为大多数*print-dup*的用例提供了更好的替代方案。由于Clojure通常是建立在几个数据抽象(列表、向量、集合、映射和通常的标量)之上的,打印/读取循环的默认行为是保留数据的抽象形状(映射是映射),但不一定是其具体类型。例如,Clojure有多个映射抽象的实现,例如PersistentArrayMap用于小型映射,而PersistentHashMap用于较大的映射。语言的默认行为假定您不关心具体类型。

对于罕见情况或更专业的类型(在定义时使用deftype或defstruct),您可能希望更多地控制它们的读取,这就是print-dup的作用。

关键是,如果将*print-dup*设置为true,则pr和相关函数将不会生成有效的EDN,而实际上是包括一些明确的# =(eval build-my-special-type)形式的Clojure数据,这些形式并不是有效的EDN。

[0]: 在"Lisp"中,编译器是明确以数据结构为基础定义的,而不是以字符串为基础。虽然这似乎与通常的编译器有些不同(它们确实在处理过程中将字符流转换为数据结构),但Lisp的定义特点是读取器发出的数据结构是语言中常用的数据结构。换句话说,编译器基本上只是语言中随时可用的一个函数。这已经不像过去那么独特了,因为大多数动态语言都支持某种形式的eval; Lisp独特之处在于eval接受一个数据结构,而不是一个字符串,这使得动态代码生成和评估变得更加容易。编译器作为“仅仅是另一个函数”的一个重要含义是,编译器实际上已经运行,并且整个语言已经被定义和可用,并且到目前为止阅读的所有代码也都可用,这打开了Lisp宏系统的大门。

5
实际上,可以通过cljs.reader/register-tag-parser注册自定义标签解析器!
对于我拥有的一个记录,它看起来像这样: (register-tag-parser! (s/replace (pr-str m/M1) "/" ".") m/map->M1)
@Gary - 非常好的答案

5

cljs.reader/read 只支持EDN格式,但是 pr 等函数会输出标签(特别是协议和记录),这些标签无法被解析。

通常情况下,如果在Clojure端你可以验证 (= value (clojure.edn/read-string (pr-str value))),那么你的cljs互操作应该能够工作。这可能有限制,因此EDN库的一些解决方案或修复措施正在讨论中。

根据你的数据形式,你可能需要查看Clojure Cookbook中描述的tagged库。


@micheal-victor-zink 感谢您确认我的假设!(即 cljs.reader/read[-read] 符合 EDN 标准。但是,是否有关于这一事实的文档?我正在查看代码,但并没有明确说明这是实际情况。 - Neoasimov
cljs.reader/read和cljs.reader/read-string有什么区别? - johnbakers
2
@hellofunk read 接受一个 PushbackReader,而 read-string 将你的字符串转换为一个 PushbackReader,然后在其上调用 read - Michael Victor Zink

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接