(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
- 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 - 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 - 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 - 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 ofemacs
relative to single language IDEs for polyglots)