The question was asked once again — so I'm writing some code into REPL and seeing the results, but that's not very convenient. So I decided to write a small intro on how to do interactive development (a.k.a. REPL-driven development) conveniently.
So, to get the elephant out of the room first: we almost never need to type anything into REPL. The only exception are commands from user.clj
file, like (start)
or (reload)
— but that's a separate subject. But we never type anything like this into REPL:
(defn add-two [n]
(+ n 2))
Moreover, we don't even type this into REPL:
(add-two 3)
So what do we do? The problem with talking about this is that by nature interactive development is difficult to explain without showing, so I guess I'll use some screenshots. I'll be using Emacs CIDER as my development environment, but the same can be achieved with VS Code and Calva, vim etc. I'm not quite sure about Intellij IDEA, so can't help there.
So let's start.
What is structural editing?
One more thing before we really start.
The main difference of LISPs from "normal" languages is the infamous homoiconicity. The fact that the code is written using the languages own data structures (mostly lists). One of the conclusions from this fact is that there are very clear boundaries between each LISP expression, or s-expression (or s-exp). The brackets.
There is a very practical effect from this fact. When you have something like this (let's pretend that |
is the cursor), your editor can easily know that it's standing right after the (+ 2 3)
s-exp, and inside the (inc ...)
s-exp.
(inc (+ 2 3)|)
It's super trivial for the editor to figure this out. In Python you'd need to count white-spaces to make sure of that, so it's a harder task. In any LISP it's trivial, just the position of the brackets counts.
Ok, now starts the fun part.
Correct way to drive the REPL
So, as I said above we are not going to be typing into the REPL ever again (except for the commands from user.clj
). But we are still going to develop interactively.
When we have an expression (+ 2 3)
, in CIDER we're going to put the cursor right after it:
(+ 2 3)|
And run a command called M-x cider-eval-last-sexp RET
(in VS Code and Calva that would be ctrl+alt+enter
for Evaluate to cursor
), and here's what we'll get:
This is the most important part. The editor has sent the s-exp to REPL, retrieved the evaluation result and showed it to us right next to the s-exp. Congratulations, this is one third of everything you need to know to start developing interactively.
So, we didn't type the expression into REPL, we evaluated the s-exp right from our source file. Big deal.
So, let's look at a real debugging example (well, semi-real). Let's say we have a function:
(defn authorized? [handler]
(fn [request]
(if (some-authrorization-logic request)
"You are authorized"
"You are NOT authorized")))
As you can see, this is a toy middleware function, that receives a request, runs it through some unknown to us authorization logic, and the returns a string, depending on the result. And this function is not working.
In "normal" languages you would need a tool like Postman to debug this function, constantly reloading your app and running through middleware. But not Clojurians, we have interactive development. Let's use just the one trick we already learned — eval last s-exp.
First of all, we want to capture a request, that's failing, so that we don't need to run Postman over and over again. That's quite easy, for example so:
(defn authorized? [handler]
(fn [request]
(def request request)
(if (some-authrorization-logic request)
"You are authorized"
"You are NOT authorized")))
Note that def we have inserted, to define a global variable request. This is a powerful debug mechanism, but a better way to use it is a tool like snitch. Anyway, we ran the request once and now we have the request in the current namespace. How do we check it?
By putting the cursor right after the word request
and running the only command we have right now to eval the s-exp right before cursor (and the smallest s-exp is one word):
And we have our request, now we can isolate that piece that we're debugging by using the same command again, but now on the (some-authrorization-logic request)
s-exp:
Ah, something with method is not right. Let's experiment some more, same trick, but now we can simplify the argument by reducing request to a map with just one key, since we have a hunch that that's the only thing this function cares about:
Hm, delete didn't work either. Let's try POST:
Bingo, we have figured out what the problem in this most stupid example was using one trick from interactive development. We never restarted our app, we never had to do a second network request (very convenient when doing this right in production), and yet we have figured out what the problem was.
And that's why it's called interactive development, and not REPL-driven development, cause try repeating that in a REPL.
Two more tricks
So, we have learned one trick — evaluating the expression before the cursor, but we need at least two more to be more or less fluent. I'm not going to do all the screenshots, just tell you about them.
By the way, while writing this I noticed that CIDER has at least 20 commands starting with cider-eval-
, I'm using only four or five.
Evaluate the top-level form we are in
In CIDER this is done with M-x cider-eval-defun-at-point RET
, you can find the similar VS Code command and more examples and videos on the subject in the eval tips article on Calva's website.
As you can see, even though the cursor is standing somewhere inside the inner form, what gets evaluated is the entire function. This helps when you're changing some code and want to check the new definition without leaving the function.
Evaluate the entire file
Most obvious command — edit whatever, and then send the entire file to the REPL to update the running program's state. In CIDER M-x cider-eval-buffer RET
, in Calva something something that you can find by following the link above.
Summary
So, I hope that I managed to give you a glimpse of how convenient interactive development can be. We eval expressions that are deep within functions we work with with substituted params to quickly find and fix bugs (even in production, using nREPL).
It's very important to say goodbye to the usual habit of living in the edit-compile-restart cycle and move to the Clojure's blissful cycle of starting the program once and then teaching it to do what you want.
Bonus tip
These two are actually a pair made in heaven. snitch
let's you instrument any variables that pass through a function with a simple one symbol substitution, so after add-two
is called at least once, n
will be available in the global scope:
;; note the asterix after defn
(defn* add-two [n]
(+ n 2))
(add-two 5)
n ;; => 5
And running (reload)
from clj-reload
will clean everything up and remove all the variables that got instrumented while playing with snitch
, making the namespaces clean again.
Are you looking for a comments section? I would love to hear your feedback, but managing a comments section is a separate job. You can reach me on Mastodon or send me an email to public@tarka.dev