Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Permit implementing methods for print-method and pprint/simple-dispatch for defrecord #684

Closed
grzm opened this issue Feb 23, 2021 · 4 comments
Labels

Comments

@grzm
Copy link

grzm commented Feb 23, 2021

Is your feature request related to a problem? Please describe.

I want to be able to print records as edn tagged literals. The typical way to do this in Clojure is to implement clojure.core/print-method (for most printing functions) and clojure.pprint/simple-dispatch (for clojure.pprint/pprint).

print-method and pprint/simple-dispatch both dispatch on class*. This works in clojure, as defrecord defines a class. Instances of types defined in babashka are clojure.lang.PersistentArrayMap maps with associated meta: :sci.impl/record true and :type as the symbol of the record type: simply adding methods to print-method and pprint/simple-dispatch won't work as we're not dispatching on class.

* print-method also dispatches on meta, and coincidentally it's also the :type metadata, but only if the :type is a keyword: they're symbols in babashka.

Here's an example of what I've done successfully in JVM Clojure and the current behavior in babashka. I want to print java.util.regex.Pattern in edn using the tagged literal #re/re. For example, the Clojure reader macro #"some-regex" would be printed to edn as "#re/re "some-regex".

(ns com.grzm.x.ppprint
  (:require
   [clojure.pprint :as pprint]
   [clojure.walk :as walk]))

(def re-tag-sym 're/re)
(def re-tag-str (str "#" re-tag-sym))

(defrecord Regex [re])

(defn regex-str [v]
  (str re-tag-str " " (pr-str (str (:re v)))))

;; pr, prn, and friends print via print-method (and print-dup)
;; print-method and print-dup dispatch on Class

(defmethod print-method Regex [v ^java.io.Writer w]
  (.write w (regex-str v)))

;; pprint has its own dispatch implementation, again dispatching on class
(defmethod pprint/simple-dispatch Regex [v] (print (regex-str v)))

(defn maybe-regex [x]
  (if (instance? java.util.regex.Pattern x)
    (->Regex x)
    x))

(defn ednify [x]
  (walk/postwalk maybe-regex x))

(defn print-example [_]
  (let [re #"some-re"
        ednified (ednify re)
        ednified-meta (meta ednified)]
    (println "prn")
    (prn re)
    (println "prn ednified")
    (prn ednified)
    (println "class ednified:" (class ednified))
    (println "meta ednified:" (pr-str ednified-meta))
    (when ednified-meta
      (println "(class (:type meta):" (class (:type ednified-meta))))))

(comment

  (print-example nil)
;; In JVM Clojure, prints
;;     prn
;;     #"some-re"
;;     prn ednified
;;     #re/re "some-re"
;;     class ednified: com.grzm.x.ppprint.Regex
;;     meta ednified: nil

  :end)

(when (= *file* (System/getProperty "babashka.file"))
  (print-example nil))
$ bb --version
babashka v0.2.8

$ uname -mprsv
Darwin 20.3.0 Darwin Kernel Version 20.3.0: Thu Jan 21 00:06:51 PST 2021; root:xnu-7195.81.3~1/RELEASE_ARM64_T8101 arm64 arm

$ bb ppprint.cljc
prn
#"some-re"
prn ednified
{:re #"some-re"}
class ednified: clojure.lang.PersistentArrayMap
meta ednified: {:sci.impl/record true, :type com.grzm.x.ppprint/Regex}
(class (:type meta): clojure.lang.Symbol

Describe the solution you'd like

I'd like to be able to override print-method and pprint/simple-dispatch the same way I would in JVM
Clojure, if this is feasible.

Describe alternatives you've considered

One alternative I've considered is dispatching on clojure.lang.PersistentArrayMap. clojure.lang.PersistentArrayMap isn't currently exposed in babashka. If it were, I could then implement

(defmethod print-dispatch clojure.lang.PersistentArrayMap [o writer] ,,,)

and dispatch (perhaps via another multimethod) on the metadata (if present). I'm not sure what the default behavior of the that dispatch would be, however: I'd prefer to call the other functions that handle printing of maps and other data structures, but most of those are private. I could potentially reimplement those private functions in some other namespace.

Another alternative may be to use keywords instead of symbols for the :type metadata to hook into the existing print-method dispatch function. I don't know what the implications for the rest of babashka would be. Also, pprint/simple-dispatch only dispatches on class: it doesn't have the conditional to inspect an object's metadata, so I'd have to figure out a different solution for pprint/pprint.

Another idea I've briefly considered is porting ClojureScript's IPrintWithWriter and PrettyWriter into some other namespace (print2?) and use print2/prn and print2/pprint instead of prn and pprint/pprint. However, before going down that route I wanted to get some feedback first.

I'm hoping I've missed something glaringly obvious and simple to do with the existing implementation 😄

@borkdude
Copy link
Collaborator

borkdude commented Feb 23, 2021

@grzm I will have a look at making the print multimethods work with babashka records. Not sure how easy this will be, but I agree that it would be nice if that would work.

Meanwhile, you can make your code write tagged-literals like this:

(defn maybe-regex [x]
  (if (instance? java.util.regex.Pattern x)
    (tagged-literal 're/re (str x))
    x))

(defn ednify [x]
  (walk/postwalk maybe-regex x))

(let [re #"some-re"
      ednified (ednify re)]
  (prn ednified)
  (pprint/pprint ednified))
$ clojure -M /tmp/print.clj
#re/re "some-re"
#re/re "some-re"

$ bb /tmp/print.clj
#re/re "some-re"
#re/re "some-re"

@borkdude
Copy link
Collaborator

One option could be to indeed expose our type as a keyword on the record:

$ bb -e '(defrecord Foo []) (defmethod print-method :user/foo [o w] (binding [*out* w] (print "foo!"))) (prn (with-meta (Foo.) {:type :user/foo}))'
foo!
$ clojure -M -e '(defrecord Foo []) (defmethod print-method :user/foo [o w] (binding [*out* w] (print "foo!"))) (prn (with-meta (Foo.) {:type :user/foo}))'
user.Foo
#object[clojure.lang.MultiFn 0x695a69a1 "clojure.lang.MultiFn@695a69a1"]
foo!

Another option could be to override the print-method defmulti with a custom implementation in sci/bb.

@grzm
Copy link
Author

grzm commented Feb 23, 2021

re: tagged-literal function: There's the glaringly obvious one for this particular case. Cheers!

@grzm
Copy link
Author

grzm commented Feb 23, 2021

Banging on tagged-literal a little more:

(defn ednify-1 [x]
  (cond
    (instance? java.util.regex.Pattern x)
    (tagged-literal 're/re (str x))

    (or (instance? Regex x))
    (let [v (str (:re x))]
      (tagged-literal 'regex/re v))
    
    (sci-record-instance? 'com.grzm.x.ppprint/Regex x)
    (let [v (str (:re x))]
      (tagged-literal 'sci.re/re v))

    :else
    x))

(defn ednify [x]
  (walk/prewalk ednify-1 x))

(defn print-example [_]
  (let [x {:foo #"some-re"
           :bar (->Regex #"some-Regex")
           :baz (with-meta {:re #"another-regex"}
                  {:sci.impl/record true, :type 'com.grzm.x.ppprint/Regex})}
        ednified (ednify x)
        ednified-meta (meta ednified)]
    (println "prn")
    (prn x)
    (println "prn ednified")
    (prn ednified)
    (println "pprint")
    (pprint/pprint x)
    (println "pprint ednified")
    (pprint/pprint ednified)))

clojure output

prn
{:foo #"some-re", :bar #re/re "some-Regex", :baz {:re #"another-regex"}}
prn ednified
{:foo #re/re "some-re", :bar #regex/re "some-Regex", :baz #sci.re/re "another-regex"}
pprint
{:foo #"some-re", :bar #re/re "some-Regex", :baz {:re #"another-regex"}}
pprint ednified
{:foo #re/re "some-re",
 :bar #regex/re "some-Regex",
 :baz #sci.re/re "another-regex"}

babashka output

prn
{:baz {:re #"another-regex"}, :bar {:re #"some-Regex"}, :foo #"some-re"}
prn ednified
{:baz #regex/re "another-regex", :bar #regex/re "some-Regex", :foo #re/re "some-re"}
pprint
{:baz {:re #"another-regex"},
 :bar {:re #"some-Regex"},
 :foo #"some-re"}
pprint ednified
{:baz #regex/re "another-regex",
 :bar #regex/re "some-Regex",
 :foo #re/re "some-re"}

instance? works just fine of course to detect the Regex record instance, so we never see #sci.re/re in babashka.

Doesn't solve the general print-method and pprint/simple-dispatch issue, but definitely solves my tagged-literal case.

@borkdude borkdude transferred this issue from babashka/babashka Mar 5, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants