【问题标题】:How to override println behavior for reference types如何覆盖引用类型的 println 行为
【发布时间】:2025-11-20 21:20:04
【问题描述】:

我有一个使用dosyncref-set 创建的循环图。当我将它传递给println 时,我得到了java.lang.*Error,正如我所期望的那样,因为它实际上是在尝试打印一个无限嵌套的结构。

我发现如果我执行(str my-ref),它会创建一些看起来像vertex@23f7d873 的东西,并且实际上并没有尝试遍历结构并将所有内容打印出来,所以这解决了直接意义上的问题,但只有在以下情况下才有帮助我对打印到屏幕上的内容非常小心。我希望能够调用(println my-graph) 让它将ref 打印为某种类型的自定义文本(可能涉及str),以及其他非参考内容。

目前我有一个自定义打印函数,它自己打印结构的每个元素并完全跳过打印ref。 (事实证明,查看vertex@23f7d873 实际上并不是很有用)。这使用起来很尴尬,并且极大地阻碍了在 REPL 中对内容进行随意检查,并且还阻止了 Emacs 检查员在我处于 swank.core/break 调试事物时查看内容。

一个细节是ref 实际上是defstruct 中的一个值,它还包含一些我正在尝试正常打印的其他内容。

所以我想知道我应该走哪条路。我看到了这些选项:

  1. 找出extend-type 并将CharSequence 协议应用于我的defstructed 结构,以便在遇到ref 时它可以正常工作。这仍然需要对结构进行逐个字段检查,并在涉及ref 时使用特殊情况,但至少它将问题定位到结构而不是包含该结构的任何内容。
  2. 找出遇到ref 时如何覆盖CharSequence 协议。这允许更本地化的行为,并允许我在 REPL 上查看循环引用,即使它不在结构内。这是我的首选。
  3. 弄清楚如何用toString 做某事,我相信在我做println 时会在某种程度上调用它。我对这个选项最无知。对其他的也很无知,但我一直在阅读Joy of Clojure,现在我都受到了启发。

同样,此解决方案应适用于 printpprint 以及尝试打印循环引用时通常会出错的任何其他内容。我应该采用什么策略?

非常感谢您的任何意见。

【问题讨论】:

  • fyi,(str my-ref) 的输出几乎可以肯定是调用java.lang.Object#toString() 的结果,详情如下:docs.oracle.com/javase/7/docs/api/java/lang/…
  • 请注意defstruct 已被defrecord 取代。此外,defstruct 不会创建真正的类型,因此它不能参与协议。
  • 我现在意识到我实际上已经在使用defrecord。不知道为什么我原来的帖子说defstruct

标签: graph clojure charsequence pprint cyclic-graph


【解决方案1】:

您要做的是创建一个新的命名空间并定义您自己的打印函数来模拟 clojure 打印对象的方式,并默认使用 clojure 的方法。

详细说明:

    创建一个不包括 pr-str 和 print-method 的 ns。 创建一个多方法打印方法(完全复制 clojure.core 中的内容) 创建一个简单地委托给 clojure.core/print-method 的默认方法 为 clojure.lang.Ref 创建一个不递归打印所有内容的方法

作为奖励,如果您使用的是 clojure 1.4.0,您可以使用标记文字,这样也可以读取输出。您需要为自定义标记和返回 ref 对象的函数覆盖 *data-readers* 映射。您还需要覆盖读取字符串的行为,以确保为 *data-readers* 调用绑定。

我为 java.io.File 提供了一个示例

 (ns my.print
   (:refer-clojure :exclude [pr-str read-string print-method]))

 (defmulti print-method (fn [x writer]
         (class x)))

 (defmethod print-method :default [o ^java.io.Writer w]
       (clojure.core/print-method o w))

 (defmethod print-method java.io.File [o ^java.io.Writer w]
       (.write w "#myprint/file \"")
       (.write w (str o))
       (.write w "\""))

 (defn pr-str [obj]
   (let [s (java.io.StringWriter.)]
     (print-method obj s)
     (str s)))

 (defonce reader-map
   (ref {'myprint/file (fn [arg]
               (java.io.File. arg))}))

 (defmacro defdata-reader [sym args & body]
   `(dosync
     (alter reader-map assoc '~sym (fn ~args ~@body))))

 (defn read-string [s]
   (binding [*data-readers* @reader-map]
     (clojure.core/read-string s)))

现在你可以像这样调用(println (my.print/pr-str circular-data-structure))

【讨论】:

  • 谢谢,除了“无法解析 var:*data-readers*”之外,我能够实现此功能。在注释掉之后,当我知道我正在打印一个循环引用时,我仍然不得不调用一个特殊函数。我希望通过覆盖__str____repr__ 函数,让clojure 自动打印我的参考文献。
  • *data-readers* 仅在您使用 clojure 1.4.0 时有效,如果对您有帮助,请务必接受答案。您还可以为引用定义 clojure.core/print-methods,但随后您正在修改全局打印方法,这可能是您想要的,也可能不是。
  • 我明白了...我还没有尝试过 1.4.0。在这个答案和这个链接之间:groups.google.com/group/clojure/browse_thread/thread/… 我能够解决我的问题。谢谢。
【解决方案2】:

我找到了我的解决方案——只需为每种特定类型创建一个重载clojure.core/print-method 的多方法,然后从多方法中调用通用函数。在这种情况下,每个多方法调用通用print-graph-element,它知道如何处理引用(它只是打印出被引用对象的哈希值,而不是试图打印出被引用对象的值)。在这种情况下,我假设 print-method 的调度函数只是(type <thing>),所以它按类型调度,这就是我编写多方法的方式。我实际上找不到任何文档说明 print-method dispatch 是如何工作的,但它的行为确实是这样的。

此解决方案允许我只输入对象的名称,例如 v0(由 (make-vertex) 在 repl 中创建,并且 print-method 由 repl 调用,从而防止我的 * 错误。

(defmethod clojure.core/print-method vertex [v writer]
  (print-graph-element v))
(defmethod clojure.core/print-method edge [e writer]
  (print-graph-element e))

【讨论】:

    【解决方案3】:

    如果您只是希望能够打印带有循环的数据结构,请尝试设置*print-level* 以限制打印机下降的深度。

    user=> (def a (atom nil))
    #'user/a
    user=> (set! *print-level* 3)
    3
    user=> (reset! a a)
    #<Atom@f9104a: #<Atom@f9104a: #<Atom@f9104a: #>>>
    

    还有一个*print-length* var,在处理无限长度的数据时很有用。

    【讨论】:

      最近更新 更多