2024 Clojure状态调查中分享你的想法!

欢迎!请查看关于页面以获取更多有关该功能的信息。

+2
语法和读取器

是否无法为clojure.core.Vec(即vector-of函数的结果)创建标记字面量?为该标签创建读取器函数是微不足道的,并且它在与read-str和EDN读取器一起使用时表现良好。但REPL总是会保留clojure.lang.PersistentVector


这个问题是在我寻找对字节十六进制字符串的原生支持,并支持惯用的Clojure(clojure.lang.ISeq和不可变性是高优先级)时出现的。

  1. 尽管有(vector-of :byte ...)的承诺,但由于无法用clojure.core.Vec清晰地往返字节数据,因此无法满足需求。
  2. 通过标记读取器和print-dup写入器,我可以为JVM原生字节数组实现往返能力,但它们不支持clojure.lang.ISeq并且可变。
  3. 常规向量是异质的,并且我不能安全地使用
    print-dup来实现我的目标。

这些选项都不具有吸引力。

2个答案

0

您所说的“但是,REPL始终会保留clojure.lang.PersistentVector”是什么意思?REPL并不会“保留”任何东西。

by
编辑 by
看起来,当在REPL中评估时,实现了PersistentVector的任何东西都会在分析阶段被捕捉。如果您定义了一个返回Vec的数据读取器,它将被评估到一个包含先前原始Vec内容boxed类型的持久向量中。如果,如Andy的例子(以及我在clojureverse [线程](https://clojureverse.org/t/tagged-literal-for-clojure-core-vec-not-possible-for-clojure/6452/6)中的初始尝试)中您绑定数据读取器和使用read-string,则不会出现这种情况。您将保留读出的Vec,因为REPL永远不会将其作为VectorExpr进行评估。但这并没有解决OP原始问题,即拥有一个生成Vec的数据_reader,该Vec不会评估到向量。我最好的办法是定义一个自定义类型来绕过向量检查,这样它就会下穿评估。但通过这种方式,存在一个关于print-dup的遗留问题。
by
是的,我选择的“winds up holding”这个词并非非常精确。在REPL中,直接读取tagged literal(*not* with `read-str`)将评估为`clojure.lang.PersistentVector`。一开始有点令人困惑,因为`clojure.lang.PersistentVector`的打印表示形式与`clojure.core.Vec`相同。
0
by

下面是一个REPL会话的样本,表明定义一个返回包含字节的`clojure.core.Vec`对象的数据读取器的方法,并使用REPL中的调用来说明它是此类型。

如果您能分享一个类似的REPL会话,其中您尝试的东西给您预期的结果为clojure.core.Vec,而当您希望它是clojure.lang.PersistentVector时,您可以在后续评论中分享它们,这可能会帮助我们确定正在发生什么。

$ clojure
Clojure 1.10.1
user=> (defn first-non-hex-char [string]
  (re-find #"[^0-9a-fA-F]" string))
#'user/first-non-hex-char
user=> (defn hex-string-to-clojure-core-vec-of-byte [hex-string]
  (if-let [bad-hex-digit-string (first-non-hex-char hex-string)]
    (throw (ex-info (format "String that should consist of only hexadecimal digits contained: %s (UTF-16 code point %d)"
                            bad-hex-digit-string
                            (int (first bad-hex-digit-string)))
                    {:input-string hex-string
                     :bad-hex-digit-string bad-hex-digit-string}))
    (if (not (zero? (mod (count hex-string) 2)))
      (throw (ex-info (format "String contains odd number %d of hex digits.  Should be even number of digits."
                              (count hex-string))
                      {:input-string hex-string
                       :length (count hex-string)}))
      ;; There are likely more efficient ways to do this, if
      ;; performance is critical for you.  I have done no performance
      ;; benchmarking on this code.  This code is taking advantage of
      ;; JVM library calls ready aware of.
      (let [hex-digit-pairs (re-seq #"[0-9a-fA-F]{2}" hex-string)
            byte-list (map (fn [two-hex-digit-str]
                             (.byteValue
                              (java.lang.Short/valueOf two-hex-digit-str 16)))
                           hex-digit-pairs)]
        (apply vector-of :byte byte-list)))))
#'user/hex-string-to-clojure-core-vec-of-byte
user=> (def bv1
  (binding [*data-readers*
            (assoc *data-readers*
                   'my.ns/byte-vec user/hex-string-to-clojure-core-vec-of-byte)]
    (read-string "#my.ns/byte-vec \"0123456789abcdef007f80ff\"")))
#'user/bv1
user=> bv1
[1 35 69 103 -119 -85 -51 -17 0 127 -128 -1]
user=> (type bv1)
clojure.core.Vec
user=> (type (bv1 0))
java.lang.Byte
by
bytevector.core> (deftype blee [x])
bytevector.core.blee
bytevector.core> #=(bytevector.core.blee. #=(vector-of :byte 1 0 1))
      在 (*cider-repl     workspacenew\bytevector:localhost:59588(clj)*:1:8145) 编译 fn* 时出现语法错误。
      无法将对象嵌入代码中,可能是 print-dup 未定义:    clojure.core$reify__8311@31c365b
bytevector.core> #=(bytevector.core.blee. 2)
#object[bytevector.core.blee 0x6e95ec6b "bytevector.core.blee@6e95ec6b"]

这可能与 print-dup 通常的工作方式有关,有关读时求值的 #= 内容,我想。
by
是的,我和 Tom 上周也注意到这种行为。我想这个特殊形式对求值有一些微妙的影响。

在某些情况下是一种不错的工作方式,但不适用于 REPL。
by
"无法将对象嵌入代码中,可能是 print-dup 未定义" 的根本原因,这个对象具有 "reify" 和一串十六进制数字的打印表示,是以下几种因素的组合

(1) Clojure 的基本向量使用 deftype 定义。

(2) 对于通过 deftype 定义的任何类型,Clojure 的 Compiler.java 源文件中都有一个 emitValue Java 方法,用于在 JVM 字节码中嵌入字面量值。您可以通过搜索文件中 "IType" 的首次出现来查找它,这是 Clojure deftype 创建的类型实现的一个 Java 接口,以便稍后识别它们是否是 deftype 创建的类的对象。当此类对象是 Clojure 代码中的字面量时,emitValue 会尝试创建在 JVM 字节码之后执行时可以构造原始值的情况,并且对于 deftype 创建的对象,它始终尝试遍历对象的全部字段,并为其和其值生成代码。

(3) Clojure的基本向量拥有一个名为"am"的字段,简称"数组管理器",这是一个通过调用Clojure的"reify"函数创建的对象。这个对象用于实现表示Clojure基本向量的树结构的"叶"节点上的多个Java方法,对于每种不同的原始类型都有一个这样的对象,因为每种原始类型的数组操作在JVM的字节码中都不同,Rich可能正是通过这种方式在运行时效率上下功夫,不是在每次操作时检测原始类型,而是有一个对象已经包含了处理该向量的原始类型的代码。

(4) 当使用"reify"调用返回的对象调用emitValue时,会尝试调用`RT.printString`,如果定义了一个`print-dup`方法来处理这种对象,那么它就会工作,但在一般情况下,"reify"返回的对象可以指向其他具有内部状态的JVM对象,或者自身具有内部状态,因此没有一种好方法可以创建一个处理所有通过调用"reify"创建的可能对象的`print-dup`定义。

关于这个问题可以如何解决呢?

可能还有很多我没有考虑到的替代方案,但以下是一些潜在的方法,其中大部分需要以某种方式修改Clojure的实现。

(方法 #1a)
修改Clojure的基本向量实现,使其所有字段值都是不可变的有打印表示的形式,即没有从'reify'返回的对象,也没有任何函数引用。因为基本向量是深度为O(log_32 n)的树,通过emitValue创建的表示将反映这种树结构,但似乎可以使其正确工作。这可能会降低基本向量操作的性能,因为在叶子节点中需要使用"case"或其他条件代码来处理不同的原始类型。

(方法 #1b)
创建Clojure基本向量的一个新的实现,它使用deftype,但具有上述#1a中建议的更改。不需要修改Clojure的实现,因为它将是一个第三方实现,可以自行做出实现选择。

(方法 #2)
修改Compiler.java中的emitValue方法,使其对于由deftype创建的对象,它会首先检查该对象的类中是否存在print-dup方法,如果存在则使用它,如果不存在则回退到当前方法。在这种情况下,这可能相当困难,因为Clojure基本向量实现了clojure.lang.IPersistentCollection接口,它已经有一个不会对基本向量起作用的print-dup方法。一个可能的方法不是简单地调用print-dup并看看结果如何,而是检查print-dup多重方法是否具有针对尝试进行emitValue的对象类的确切实现(例如,对于基本向量,clojure.core.Vec)。这种对多重方法实现在Clojure中的确切类检查似乎违反了Clojure中多重方法的精神,并且看起来有些不靠谱。

对这个想法的另一个更简洁的变化是,在Clojure的实现中定义一个新的"emittable"接口,如果由deftype创建的类实现了它,那么emitValue就会使用实现了该接口的对象的'emit'方法。

(方法 #3)
创建一个不使用deftype也不使用defrecord的独立的Clojure基本向量实现,它退回到Clojure的emitValue的最后"else"案例,长长的if-then-else链。这对我来说似乎很困难,或者可能不可能,因为emitValue当前在最后的"else"之前有一个clojure.lang.IPersistentVector的case,尝试创建一个不实现该接口的Clojure基本向量实现将非常奇怪。

在我的想法中,方案#1b,或者是方案#2的最后一个变体,可能可行。#1b不需要对Clojure的实现进行任何修改。#2则肯定需要。方案#3可能不是一个真正可行的替代方案,理由在上面已经说明。

更多细节可以在本仓库的README中找到:[点击查看](https://github.com/jafingerhut/vec-data-reader)
Andy,你的分析符合我的经验,你很好地表达了这个问题,并提出了合理的解决方案。谢谢。

我认真考虑了#1b(因为其他方案超出了我的薄弱Java技能),甚至还尝试了一个原型实现。我遇到的一个挫败是一旦我使用了任何存储数据的底层类型(在我的情况下,是一个通过我实现的IPersistentVector/assocN强制同质性的持久字节向量),我就需要这个类型实现ISeq。但当我这样做的时候,我就失去了控制打印的能力。很抱歉我记不起那次实验的更多细节...也许我可以让它复活。

(边注:也许与你在#2中的观察有关,但是一条有趣的旁路:clojure.core.Vec的_print_method是通过什么方式确定的?这条链接([点击查看](https://github.com/clojure/clojure/blob/master/src/clj/clojure/gvec.clj#L455))似乎是关键所在,但我不知道::Vec如何发挥作用,实际上,当我覆盖(可能)Vec的print方法时,我并没有使用全局层次结构——我只对类进行defmethod。我的猜测是,就像我对自己类型的尝试一样,它永远没有被调用过。)

我的最终目标是支持一个十六进制字符串字面量(与REPL兼容)的读取器和打印机,该读取器和打印机背后支持在向量上执行惯用的Clojure操作的类型。

再次感谢你对这个问题的深入分析。

编辑后,阅读你的仓库让我清楚地了解了我在什么时候以及为什么失去了控制我的deftype的打印。
我不明白为什么gvec.clj中的print方法具有`::Vec`的分发值——我本来预计它应该是`clojure.core.Vec`类,但我可能没有足够的上下文来理解它为什么会是`::Vec`。

您可以使用 `(methods print-method)` 来查看所有为它们定义了 `defmethod` 的分发值 -- 它将是映射的键,所有这些键都是类和接口名称,其中只有两个在 gvec.clj 中是关键字。  您可以使用 `(get-method print-method <some-expression>)` 来查看针对特定值将调用哪个方法。  如果您想了解对应哪个分发值,您可以手动在 `(methods print-method)` 的输出中找到它,或者编写一些代码来为您找到。

如果您仍然拥有 #1b 的试验实现,我可能有一些时间来看一看,以防我注意到任何不妥之处,但无法保证。
...