Clojure: intro to tap> and accessing private vars


Clojure 1.10 introduced a new system called tap. From the release notes: tap is a shared, globally accessible system for distributing a series of informational or diagnostic values to a set of (presumably effectful) handler functions. It can be used as a better debug prn, or for facilities like logging etc.

Tap has a nice simple api. We can send a value to the set of registered handler functions with tap>. We can register a handlers function with add-tap. Finally, we can unregister a handler function with remove-tap.

Adding a tap handler and sending values

Create an atom bar and register an anonymous handler function to the tap system with add-tap. This will conj any values we pass to tap> to the bar atom.

(def bar (atom []))

(add-tap (partial swap! bar conj))

(tap> (inc 1))

@bar

=> [2]

(tap> "foo")

@bar

=> [2 "foo"]

When we de-reference bar we get the values [2 "foo"] that have been passed to tap>. What happens if we add the same anonymous handler function to the tap system again?

(reset! bar [])

(add-tap (partial swap! bar conj))

(tap> "foo")

@bar

=> ["foo" "foo"]

Surprisingly, even though we called tap once, two "foo"s got written to our atom. Let's investigate the add-tap source and work out what's going on.

(source add-tap)

(defn add-tap
  [f]
  (force tap-loop)
  (swap! tapset conj f)
  nil)

So, add-tap adds the tap handlers to an atom called tapset. From the name, we can guess that it might be a set which means we shouldn't be able to register the same tap function twice. Let's try and access tapset.

clojure.core/tapset

=>
Syntax error compiling at (form-init1817879857542651664.clj:1:1).
var: clojure.core/tapset is not public

No luck, tapset is not public.

Creating and accessing private vars

In Clojure you can create private vars by adding the key :private to a var's metadata.

(def ^:private private-var "foo")

=> #'user/private-var

(ns baz)

user/private-var

=>
Syntax error compiling at (form-init1817879857542651664.clj:1:1).
var: user/private-var is not public

Even though these private vars are not intended to be accessed, we can work around this by using #' to refer directly to the var. We can then de-reference it to access its value.

#'user/private-var

=> #'user/private-var

@#'user/private-var

=> "foo"

There are rarely any reasons to ever have to do this in production code, and even then it would not be advisable. However, it can be very useful when exploring a new api in the repl.

Back to tapset

Armed with our new knowledge of how to access private vars we can find out what's in tapset. Notice the @@ we need to derefence tapset twice: once to get the value of the var, and once to get the value of the atom.

@@#'clojure.core/tapset

=> #{#object[clojure.core$partial$fn__5831 0x18852ca3
             "clojure.core$partial$fn__5831@18852ca3"]
     #object[clojure.core$partial$fn__5831 0xb50d66f
             "clojure.core$partial$fn__5831@b50d66f"]}

It looks like our anonymous functions are not unique and therefore count as different functions as far as tap is concerned. Let's reset! the tapset.

(reset! bar [])
(reset! @#'clojure.core/tapset #{})

(tap> "foo")

@bar

=> []

Back to normal. So if we want to be able to prevent the same function from getting added multiple times, we need to give it a name.

(defn conj-to-bar [x]
  (swap! bar conj x))

(add-tap conj-to-bar)
(add-tap conj-to-bar)

@@#'clojure.core/tapset

=> #{#object[user$conj_to_bar 0x4f1e0067 "user$conj_to_bar@4f1e0067"]}

Even though we called the add-tap function twice with the same function, it only got added once.

Removing tap handlers

The other advantage of using named functions is that you can use remove-tap to remove tap functions from the tapset. With an anonymous function you would have to hang on to a reference to be able to remove it from the tapset.

(remove-tap conj-to-bar)

@@#'clojure.core/tapsetg

=> #{}

This concludes this initial intro to Clojure 1.10's tap system and some useful tricks for accessing private vars.