Quickly define print handlers for tagged literals across print/pprint implementations.
data-printers is part of a growing collection of quality Clojure libraries and tools released on the Lambda Island label. If you are using this project commercially then you are expected to pay it forward by becoming a backer on Open Collective, so that we may continue to enjoy a thriving Clojure ecosystem.
One stop shop registration of print handlers
deps.edn
lambdaisland/data-printers {:mvn/version "0.0.8"}
project.clj
[lambdaisland/data-printers "0.0.8"]
With Clojure/ClojureScript we get a serialization format for free, out of the box. EDN or Extensible Data Notation is a subset of the Clojure syntax format, which can represent all of Clojure's built-in primitives and collections.
In LISP parlance serializing data is called "printing", and deserializing is referred to as "reading".
(pr-str {:x 1 :y 2})
;;=> "{:x 1 :y 2}"
(read-string "{:x 1 :y 2}")
;;=> {:x 1 :y 2}
Types that are not native to Clojure however will throw a spanner in the works. They will print in a way that is noisy and opaque, and what is worse: this printed version is no longer valid EDN. It will fail to be read back.
(deftype CustomType [x])
(pr-str (->CustomType 1))
;; => "#object[my.ns.CustomType 0x49763a36 \"my.ns.CustomType@49763a36\"]"
(read-string "#object[my.ns.CustomType 0x49763a36 \"my.ns.CustomType@49763a36\"]")
;; java.lang.RuntimeException
;; No reader function for tag object
To solve this EDN provides tagged literals, for instance the value
(->CustomType 1)
could be represented as #my.ns/CustomType {:x 1}
. Now this
is valid EDN, and you get to see what you are dealing with. For a REPL-based
workflow, or when wading through test results being able to see the actual
values is invaluable. (pun intended) To make this work you need to teach both
the reader and the printer about the tag and the type.
Dealing with the reader is relatively straightforward, you do this by supplying
a data_readers.cljc
, or by binding *data-readers*
, see the Tagged
Literals section in the
official Clojure docs.
When it comes to printing however the situation is more complex. There are multiple printer implementations, and significant differences between Clojure and ClojureScript. This is where this library comes in. It aims to make it easy to define print handlers, and to have them set up across implementations, so you always get consistent results.
Clojure and ClojureScript contain two built-in printers, the one that powers all
the pr
functions (pr
, prn
, pr-str
, etc), and clojure.pprint
. On the
Clojure side you need to extend multimethods, for ClojureScript you implement a
protocol. We pave over these differences by providing a number of functions all
with the same signature.
(register-print CustomType 'my.ns/CustomType (fn [obj] {:x (.-x obj)}))
This takes the type (java.lang.Class
or JavaScript constructor function), a
tag as a symbol, and a function returning a plain EDN representation of
instances of the type.
Available functions:
lambdaisland.data-printers/register-print
lambdaisland.data-printers/register-pprint
lambdaisland.data-printers.puget/register-puget
lambdaisland.data-printers.deep-diff/register-deep-diff
lambdaisland.data-printers.deep-diff2/register-deep-diff2
lambdaisland.data-printers.transit/register-write-handler
You should create a wrapper in your own project where you call all the ones that apply to you, depending on your project's dependencies. Here's a full example:
(ns lambdaisland.data-printers.example
(:require [lambdaisland.data-printers :as dp]
[lambdaisland.data-printers.deep-diff :as dp-ddiff]
[lambdaisland.data-printers.deep-diff2 :as dp-ddiff2]
[lambdaisland.data-printers.transit :as dp-transit]
[lambdaisland.data-printers.puget :as dp-puget]))
(defn register-printer [type tag to-edn]
(dp/register-print type tag to-edn)
(dp/register-pprint type tag to-edn)
(dp-puget/register-puget type tag to-edn)
(dp-ddiff/register-deep-diff type tag to-edn)
(dp-ddiff2/register-deep-diff2 type tag to-edn)
(dp-transit/register-write-handler type tag to-edn))
This can be a .cljc
file, the platform-specific handlers are all still
implemented as CLJC for your convenience, even though they may do nothing on a
given platform.
This library only provides registration of write/print handlers, since Clojure already comes with fairly convenient support for custom tagged literal readers, but some caveats you should be aware of.
Reader functions are declared in a data_readers.clj
, data_readers.cljs
, or
data_readers.cljc
file. This should contain a map from tag (symbol) to
function name.
{my.ns/my-type my.ns/type-reader-fn}
When booting Clojure will create the my.ns
namespace object, and the
my.ns/type-reader-fn
var object, but it will not actually load the namespace.
The var will initially be undefined/empty, so you have to make sure to
(:require [my.ns])
before Clojure encounters its first #my.ns {}
tagged
literal.
Note that this may also confuse code that uses requiring-resolve
. This will
return the undeclared/empty var, without requiring the namespace!
When only dealing with Clojure your reader function can just return the value it needs to return and you're done, but when dealing with ClojureScript or cross-platform code it gets a bit more tricky.
ClojureScript reader functions are still declared in Clojure (which makes sense,
the ClojureScript compiler is written in Clojure, and handles the reading,
compiling, and generating JS). In this case the function should return a
form. Think of it as a macro, but defined with defn
instead of defmacro
.
(defn my-reader [obj]
`(->MyType (:x ~obj)))
If the same form is valid Clojure and ClojureScript then you are good to go, if not then this helper can come in handy:
(defmacro platform-case [& {:keys [cljs clj]}]
`(if (:ns ~'&env) ~cljs ~clj))
Used as such:
(defn my-reader [obj]
(platform-case :clj `(->MyType (:x ~obj))
:cljs `(->MyCljsType (:x ~obj))))
Transit is the odd one out here, since it's not EDN, and because it requires some extra care and handling.
Transit does not come with a registry of handlers that you can easily add to, instead you need to pass the handlers when creating a writer.
(require '[cognitect.transit :as transit])
(def writer (transit/writer :json {:handlers @dp-transit/write-handlers}))
Since in the case of Transit you will probably also want to read back your
serialized data, we include a macro to turn your data_readers.cljc
into
transit readers. (currently only .cljc
is supported)
(def reader (transit/writer :json {:handlers (dp-transit/data-reader-handlers)}))
The way this works is it will call your read handler functions, passing in a symbol. It expects to get a valid form back which gets turned into a transit read handler, so make sure your data readers are defined as described above, as macros in disguise.
Everyone has a right to submit patches to data-printers, and thus become a contributor.
Contributors MUST
*
**
Contributors SHOULD
If you submit a pull request that adheres to these rules, then it will almost certainly be merged immediately. However some things may require more consideration. If you add new dependencies, or significantly increase the API surface, then we need to decide if these changes are in line with the project's goals. In this case you can start by writing a pitch, and collecting feedback on it.
*
This goes for features too, a feature needs to solve a problem. State the problem it solves, then supply a minimal solution.
**
As long as this project has not seen a public release (i.e. is not on Clojars)
we may still consider making breaking changes, if there is consensus that the
changes are justified.
Copyright © 2021 Arne Brasseur and Contributors
Licensed under the term of the Mozilla Public License 2.0, see LICENSE.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close