|
| 1 | +^{:kindly/hide-code true |
| 2 | + :clay {:title "Clean object printing by removing extraneous" |
| 3 | + :quarto {:author :timothypratley |
| 4 | + :type :post |
| 5 | + :date "2025-06-05" |
| 6 | + :category :clojure |
| 7 | + :tags [:print-method :objects]}}} |
| 8 | +(ns clojure.print-object.remove-extraneous |
| 9 | + (:require [clojure.core.async :as async] |
| 10 | + [clojure.string :as str]) |
| 11 | + (:import (clojure.lang MultiFn) |
| 12 | + (java.io Writer))) |
| 13 | + |
| 14 | +^:kindly/hide-code ^:kind/hidden |
| 15 | +(set! *warn-on-reflection* true) |
| 16 | + |
| 17 | +;; The Clojure default for printing objects is noisy. |
| 18 | +;; Clojure's `print-method` for `Object` delegates to `clojure.core/print-object` |
| 19 | + |
| 20 | +(defmethod print-method Object [x ^java.io.Writer w] |
| 21 | + (#'clojure.core/print-object x w)) |
| 22 | + |
| 23 | +(Object.) |
| 24 | + |
| 25 | +;; The syntax is `#object[CLASS-NAME HASH toString())]` |
| 26 | +;; and as you can see, the toString of an Object is `CLASS-NAME@HASH`. |
| 27 | +;; For most objects this becomes quite a long string. |
| 28 | + |
| 29 | +(async/chan) |
| 30 | + |
| 31 | +;; Functions are printed as objects |
| 32 | + |
| 33 | +(fn [x] x) |
| 34 | + |
| 35 | +;; It's quite easy to miss the fact that it is a function as we are looking for a tiny little `fn` in a sea of text. |
| 36 | +;; If, like me, you are fond of the [odd lambda calculus excursion](/code_interview/beating/with_stupid_stuff/z_combinator_gambit.html), |
| 37 | +;; things get even more hectic. |
| 38 | + |
| 39 | +((fn [x] (fn [v] ((x x) v))) (fn [y] y)) |
| 40 | + |
| 41 | +;; Yikes! what an eyesore. |
| 42 | +;; This is not an academic issue specific to lambda calculus. |
| 43 | +;; Any function created from inside a function is helpfully identifiable through the `fn$fn` nesting. |
| 44 | +;; We create these quite regularly, and they are often printed in stack traces. |
| 45 | +;; I'm sure you have seen them when you map an inline function across a seq, and there is a bug in the anonymous function. |
| 46 | + |
| 47 | +(defn caesar-cipher [s] |
| 48 | + (mapv (fn add2 [x] (+ 2 x)) s)) |
| 49 | + |
| 50 | +(try (caesar-cipher "hello world") |
| 51 | + (catch Exception ex |
| 52 | + (vec (take 4 (.getStackTrace ex))))) |
| 53 | + |
| 54 | +;; See that part `caesar_cipher$add2`? |
| 55 | +;; That is **very** useful information. |
| 56 | +;; It tells us that the exception was inside `add2`, which is inside `caesar-cipher`. |
| 57 | +;; The stack trace doesn't print functions as objects, |
| 58 | +;; I'm just pointing out that the thing that we care about for functions is really just that they are a function, |
| 59 | +;; what their name is, and whether they were created from inside another function. |
| 60 | +;; Let's go back to why printing a function as an object. |
| 61 | +;; An easy improvement is to demunge from Java names to Clojure names. |
| 62 | +;; Demunging converts `_` to `-` and `$` to `/`, and munged characters like `+` which is `PLUS` in Java. |
| 63 | + |
| 64 | +(defn class-name |
| 65 | + [x] |
| 66 | + (-> x class .getName Compiler/demunge)) |
| 67 | + |
| 68 | +(class-name ((fn [] (fn [y] y)))) |
| 69 | + |
| 70 | +;; Next, we don't need the eval identities. |
| 71 | + |
| 72 | +(defn remove-extraneous |
| 73 | + "Clojure compiles with unique names that include things like `/eval32352/` and `--4321`. |
| 74 | + These are rarely useful when printing a function. |
| 75 | + They can still be accessed via (class x) or similar." |
| 76 | + [s] |
| 77 | + (-> s |
| 78 | + (str/replace #"/eval\d+/" "/") |
| 79 | + (str/replace #"--\d+(/|$)" "$1"))) |
| 80 | + |
| 81 | +(remove-extraneous (class-name ((fn [] (fn [y] y))))) |
| 82 | + |
| 83 | +;; Much nicer. |
| 84 | +;; I can actually read that! |
| 85 | +;; I'm not particularly fond of the long namespace shown, |
| 86 | +;; and multiple slashes form invalid symbols, |
| 87 | +;; so I prefer using $ as the name level delimiter. |
| 88 | + |
| 89 | +(defn format-class-name ^String [s] |
| 90 | + (let [[ns-str & names] (-> (remove-extraneous s) |
| 91 | + (str/split #"/"))] |
| 92 | + (if (and ns-str names) |
| 93 | + (str (str/join "$" names)) |
| 94 | + (-> s (str/split #"\.") (last))))) |
| 95 | + |
| 96 | +(format-class-name (remove-extraneous (class-name ((fn [] (fn [y] y)))))) |
| 97 | + |
| 98 | +;; So short, so sweet. |
| 99 | + |
| 100 | +(defn object-str ^String [x] |
| 101 | + (str (if (fn? x) "#fn" "#object") |
| 102 | + " [" (format-class-name (class-name x)) "]")) |
| 103 | + |
| 104 | +(object-str ((fn [] (fn [y] y)))) |
| 105 | + |
| 106 | +(object-str (async/chan)) |
| 107 | + |
| 108 | +;; This is really all I care to know about when printing objects and functions, |
| 109 | +;; and it matters inside notebooks, |
| 110 | +;; where we want to print things, eval things that return objects and functions, |
| 111 | +;; and datafy complex objects that contain other objects. |
| 112 | +;; To print things without knowing if they are objects, functions, or data, |
| 113 | +;; we can extend Clojure's `print-method`. |
| 114 | + |
| 115 | +(defmethod print-method Object [x ^Writer w] |
| 116 | + (.write w (object-str x))) |
| 117 | + |
| 118 | +((fn [] (fn [y] y))) |
| 119 | + |
| 120 | +(async/chan) |
| 121 | + |
| 122 | +;; You can require this namespace from other notebooks to turn on this nice, concise mode of object printing. |
| 123 | + |
| 124 | +;; Happy notebooking! |
0 commit comments