I Tracing Clojure with Contrail. i
One of the great things about the “better is
better”1 ethos in the
Common Lisp community is that it’s given rise to a culture of very
good tools. For example, most implementations extend
trace
in interesting
ways. In
particular, SBCL
provides a number of useful options which I’ve missed when working in
Clojure. (There are a number of tools in this space for Clojure, most
notably clojure.tools.trace,
which ships with Clojure, but their functionality is limited.)
A little while ago I began building Contrail, which is an attempt to bring some of these features (as well as others that might prove useful) to Clojure. It’s built using the Richelieu advice (in the Emacs Lisp sense) library, with a few tweaks to support advice that persists when an interactively traced var is re-defined (typically by the user re-compiling her source file).
This “persistent trace” feature is a good example, in fact, of some of
the tradeoffs between the “better is better” approach (with its focus
on interface simplicity) and the now-dominant2 “worse is better” ethos (with its
focus on implementation simplicity). I wanted a simple interface: when
you, the user, invoke the trace
function on a var, it stays traced
until you subsequently call untrace
.
In particular, you shouldn’t need to think about the fact that the
tracing is implemented by re-binding the traced var to a modified copy
of the function to which it was originally bound, nor should you have
to worry about whether recompiling the var’s original defn
will blow
away that binding and thus your trace.
It turns out that this simple interface is bought at the cost of
substantial implementation complexity. It’s no longer possible to
store all of the information regarding trace state on the traced var
itself (since defn
replaces not only the existing value of a var but
its metadata as well), so now I need to store the set of traced vars
separately.
Additionally, it becomes necessary to monitor changes to the var’s value and re-trace it as required–and of course I need to distinguish between changes made by the tracing library (e.g. when a user explicitly untraces) and those made elsewhere:
But it’s worse than that; I’ve also got to handle the case where a var that has been explicitly untraced by the user gets accidentally bound to its old, traced value (this is a situation which with-redefs
makes it very easy to get into). So I’ve got to check for that on every invocation of the trace reporting machinery:
There is a non-trivial amount of implementation complexity involved in
supporting the simple interface to trace
. Is this the right
tradeoff? I’m not sure. I think the answer mostly comes down to
whether I’ve hidden that complexity successfully, or whether trace
is a leaky abstraction. As the author I’m very poorly positioned to
answer that question, so I’ll have to see what happens when and if
Contrail picks up other users.
-
Gabriel calls this the “the right thing” ethos, but I think this connotes something more rigid and deterministic than the design process and belief system which he is actually describing. ↩
-
There are notable exceptions, of course: Apple frequently attempts to build simple interfaces concealing implementation complexity (e.g. Handoff, various cross-device syncing) with mixed results. ↩