Skip to content

Commit c7afc24

Browse files
nicer object printing
1 parent cd3c7aa commit c7afc24

File tree

8 files changed

+135
-63
lines changed

8 files changed

+135
-63
lines changed

notebooks/clojure+/print/objects_and_protocols.clj

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
[clojure.string :as str]
1010
[core.async.flow.example.stats :as stats]))
1111

12-
;; # Printing Objects in Clojure
13-
;;
14-
;; The Clojure default for printing objects is noisy:
12+
;; The Clojure default for printing objects is noisy.
13+
;; Clojure's `print-method` for `Object` delegates to `clojure.core/print-object`
14+
15+
(defmethod print-method Object [x ^java.io.Writer w]
16+
(#'clojure.core/print-object x w))
1517

1618
(Object.)
1719

@@ -240,9 +242,3 @@
240242
;; All of that to say, do we really want those unique identifiers printed out?
241243
;; No! If we need to find them, we can always look them up another way.
242244
;; We don't need them polluting our REPL output.
243-
244-
^:kindly/hide-code
245-
(comment "reset the printer back for testing")
246-
^:kind/hidden ^:kindly/hide-code
247-
(defmethod print-method Object [x ^java.io.Writer w]
248-
(#'clojure.core/print-object x w))
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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!

notebooks/code_interview/beating/with_stupid_stuff/z_combinator_gambit.clj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
:category :clojure
88
:tags [:lambda-calculus :code-interview]
99
:keywords [:z-combinator]}}}
10-
(ns code-interview.beating.with-stupid-stuff.z-combinator-gambit)
10+
(ns code-interview.beating.with-stupid-stuff.z-combinator-gambit
11+
(:require [clojure.print-object.remove-extraneous]))
1112

1213
;; Welcome back code champs, number ninjas, and data divers to our first episode of Beating Code Interviews with Stupid Stuff.
1314
;; People often send me emails asking, "How can I use lambda calculus to impress people?"

notebooks/core/async/flow/example/stats.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
:clay {:title "Core Async Flow Stats Example"
33
:quarto {:author [:alexmiller :timothypratley]
44
:type :post
5-
:date "2025-06-01"
5+
:date "2025-05-15"
66
:category :clojure
77
:tags [:core.async :core.async.flow]}}}
88
(ns core.async.flow.example.stats

notebooks/core/async/flow/exploration.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
:clay {:title "Core Async Flow Exploration"
33
:quarto {:author [:daslu :timothypratley]
44
:type :post
5-
:date "2025-06-02"
5+
:date "2025-05-16"
66
:category :clojure
77
:tags [:core.async :core.async.flow]}}}
88
(ns core.async.flow.exploration

notebooks/core/async/flow/visualization.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
:clay {:title "Core Async Flow Visualization"
33
:quarto {:author [:daslu :timothypratley]
44
:type :post
5-
:date "2025-06-03"
5+
:date "2025-05-17"
66
:category :clojure
77
:tags [:core.async :core.async.flow]}}}
88
(ns core.async.flow.visualization

notebooks/core/async/flow/visualization/flow_elk.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
:author :timothypratley
44
:category :clojure
55
:tags [:core.async :core.async.flow]}}}
6+
67
(ns core.async.flow.visualization.flow-elk
78
(:require [clojure.datafy :as datafy]
89
[clojure.string :as str]))

notebooks/print/object/remove_extraneous.clj

Lines changed: 0 additions & 50 deletions
This file was deleted.

0 commit comments

Comments
 (0)