Reveal 1.0: Read Eval Visualize Loop for Clojure

Hi there! I made a thing — Reveal :tada:

It’s a repl (and a library to build repls) that enables powerful inspections of objects that live in the JVM. The main ideas behind Reveal are:

  • the tool exists in the VM, which allows it to access objects directly and be available to the whole ecosystem no matter the editor/IDE;
  • printed string representations of objects hold references to objects themself, allowing to select any value and evaluate the code directly on the value;
  • an extensible set of actions and data representations can be suggested for any value, drastically improving the visibility of data;
  • users can extend Reveal easily and declaratively: reactive views can be defined by pure functions that take and return data structures.

Here is a little demo:

What do you think?

25 Likes

This is really cool, almost exactly what I’ve been wanting.

Is there a way to display big maps collapsed? Currently it seems to want to display the whole structure at once, which means it keeps trying to display my whole datomic db.

Also, do you have any plans for clojurescript? Maybe integrating with shadow.remote?

Good point about big maps! This is a default behavior that for printing is usually overridden by extending print-method for specific data types. Since Reveal’s printing exists independently of Clojure’s printing, there is an unfortunate consequence: you need to override Reveal printing yourself. I will think if there is something that Reveal can do about that.

Regarding ClojureScript: Reveal can already talk to remote processes with its remote-prepl, although the exploration capabilities will be limited to inspecting data structures received from the network, which is not the same as objects in the target VM.

1 Like

Thanks.

Maybe just adding something like *print-length* or *print-level* to Reveal would help. I’ll play around with it a bit more to see what works.

It turned out to be pretty easy to just wrap my big maps into short strings using rx/as. Which is very cool, because now I can still drill down into the big maps if I need to using view:table.

Here’s what I did. I was very surprised by how little code this needed for such powerful functionality.

[vlaaad.reveal.ext :as rx]
(defn wrapped-simple-rep [obj-type orig keys-to-keep]
  (rx/as
   orig
   (rx/vertical
    (rx/raw-string
     (str "#" obj-type "<wrapped-simple-rep>"))
    (when (seq keys-to-keep)
      (rx/entries
       (select-keys orig keys-to-keep))))))

(rx/defstream ::big-map
  [bm]
  (wrapped-simple-rep "big-map" bm [:cool]))

(def big-map
  (-> {:big-map (range 1000) :cool :info}
      (with-meta {:vlaaad.reveal.stream/type ::big-map})))
(tap> big-map)

I’m very impressed with how simple your tool is to customize, and the potential for its use is big, in my opinion.

Combined with structured logging like Cartus, this works great. I can now drill into any log message to get the context and the examine the actual objects.

And combined with lexikon, I can now even examine the lexical scope of any log message.

Very exciting stuff.

3 Likes

This looks amazing! I haven’t taken it for a spin yet, but from the looks of it this will be a huge advantage.

Thank you so much for bringing these two libraries to my attention. And I completely agree, in the context of Reveal they make even more sense.

1 Like

And thank you for publishing this piece of tech @vlaaad!

1 Like

It warms my heart to see that people use and enjoy Reveal :smile:

A little comment on your snippet: you don’t need to call rx/as inside the defstream blocks since Reveal will mark emitted regions with the value anyway. Also, you shouldn’t use (when ...) as an argument to rx/vertical — it expects functions and not nulls.

I would write it like that:

(require '[vlaaad.reveal.ext :as rx])

(rx/defstream ::big-map [m]
  (rx/horizontal
    (rx/raw-string "#big-map{" {:fill :object})
    (rx/entries (select-keys m (:visible-keys (meta m))))
    (rx/raw-string "}" {:fill :object})))

(def big-map
  (-> {:big-map (range 1000) :cool :info}
      (with-meta {:vlaaad.reveal.stream/type ::big-map
                  :visible-keys [:cool]})))

You can then build on it even more stuff if necessary. For example, it can show remaining keys annotated with corresponding values, and then you can write a contextual action that allows looking at the value for a selected remaining key. I also think it’s nice that the big-map function actually does not depend on Reveal: it’s just namespaced keywords in the meta, which means you can leave this metadata in production code and it won’t cause any trouble, while in the dev environment it will be picked up by Reveal.

2 Likes

Hi,
I got a legacy Java app with huge code base.
Sometimes I use Clojure REPL to poke in that code. However I don’t yet how to query objects in memory. Do you think it is possible to do this using your tool?

Just some feedback: I have used Reveal for the past couple of weeks, and already it has helped me solve 2 very difficult bugs that have been plaguing me for the past couple of months (related to unanticipated printing of huge data structures crashing the JVM). Super useful tool.

My favorite part is how easy it is to customize and add application-specific custom functionality. I’ve been steadily working on removing all printing from our application, and replacing it with tap> to Reveal.

Thanks for this!

1 Like

I’m not aware of any way to query for objects in the heap, but I used Clojure REPL with Reveal to poke at a java program before, I did it by having a custom main that starts the java app and also saves a reference to the application data container (e.g. Guice injector) in a user ns. Then I used that def to lookup services and over stuff.

1 Like

@vlaad Thank you for reveal. I ended up here after googling for “clojure reveal guice repl” and looks like you have done exactly that. I was wondering if you can share with me the code snippet for the same.
looking forward to hearing from you.

My pleasure :smile:

I can’t find the code right now, but it was something like this:

Injector theInjector = ...
((clojure.lang.Var) clojure.java.api.Compiler.var("user" "injector")).bindRoot(theInjector);

and the user ns looked like this:

; unbound var, set from java
(def ^com.google.inject.Injector injector)

(defn app [] 
  (.getInstance injector ...))

Then, in the repl I could call (app) and it would return the value from the injector