Share the nitty-gritty details of your Clojure workflow!

(I can’t really separate my workflow from my preferred style of
program construction, so this answer contains some of each.)

Exploratory

The beginning of most new projects for me is trying to establish an
understanding of the problem domain. So, for example, I might fetch
something from a remote JSON endpoint using clj-http and have a look
at what it returns. The process for this would be to type the fetching
code (which converts the result into Clojure data structures, include
keyword-izing it) into a working buffer in emacs and then execute it
with cider-pprint-eval-last-sexp. This will pop up a new temporary
buffer containing a pretty printed version of the result, which I will
then inspect visually.

I’ll almost certainly need to extract and/or transform some part(s) of
the returned structure to use for further processing, so I’ll move it
into a let form in my working buffer. If the data is too big to use
as a literal, I’ll just save it as an edn file and use
slurp/read-string to bring it into the let.

Once I’ve got my data bound, I write the code to interrogate it within
that scope using cider-eval-defun-at-point, which determines the
outermost sexp and evaluates that. This is bound to a single keypress
so I very quickly iterate on the code within the let until it does
the right thing. This sort work also sometimes involves using
cider-eval-last-sexp to evaluate subcomponents of a given form so I
can dissect and verify its pieces as I go without retyping anything.

Because there’s typically no state outside of the let, code that
works in that scope also works as a pure function, and once the code
in the let works, I hoist it out to a function and try calling it
using the same test data from within the scope of the let.

Bottom up

I’ll repeat this process for each small piece I need until I have a
set of working functions with which to compose higher level functions
that get more of the work done. The process of writing those higher
level functions mirrors that of writing the lower level ones: start in
a let with some stub data, get the functions right, hoist them out.

If at some stage my expectations are violated, meaning my mental model
of the data/computation is incorrect, I back up and re-check my
previous findings by re-executing my functions against the test data
until I figure out where I went wrong. Debugging should always be
about achieving a more complete understanding, not just getting tests
to pass.

Useful residue

All the stub data used in this process is retained in the working
buffer, usually in one or more comment blocks. Once I’m fairly certain
that the functions are in good enough order to deserve tests (that is,
they work and are unlikely to change anytime soon), I move these
captured bits of data into tests.

There’s often a set of functions and/or blocks of code that continue
to be useful for additional exploration but have not yet been factored
into the main program in my working buffer(s). If it’s a small
quantity that’s relevant only to one namespace, I usually put in in a
comment block at the bottom of that namespace. OTOH, if it’s the sort
of project where there’s a significant quantity of code one expects to
run manually but that isn’t really part of the program per se, I’ll
move that code into a file with a name like repl.clj and comment it
heavily.

Notes

  1. the time it takes to test each code change should be as close as
    possible to zero, thus I resist anything that involves a context
    change (changing windows/buffers, copy/pasting more than absolutely
    needed, etc) – flow states are sacred
  2. hypothesis testing is a big part of this process, and learning
    about the data and its shape is central to the act of writing the
    program
  3. the parts of the program that accumulate in the buffer work, none
    of the code is written in anticipation of need, but rather in
    response to it, and at any stage I can execute what I have so far
  4. the keybindings I use to perform these operations are the same for
    every dynamic language I use, so my process is similar from Clojure
    to Racket to OCaml to Ruby to Python (an advantage of emacs
    relative to single language IDEs for polyglots)
9 Likes