I don't mean the REPL is stateless, but the code developed using the REPL would ideally be stateless. Stateless code would be easier for others to experiment with in their own REPL, without "oh no, you broke production with your REPL!"
You can use a stateful repl to develop stateless code, but in order to provide the features of an old-fashioned Lisp or Smalltalk system, the kind of system I prefer to work with, you can't make any stateful changes off limits because there's no way to know in advance what changes you're going to need to make.
As you're rummaging around through the dynamic state of the running system, you'll discover changes that need to be made, and you might discover them absolutely anywhere. Sure, you could always kill the running system, make the change to the sources, and rebuild the system, but that's exactly what we're trying to avoid.
Consequently, old Lisp and Smalltalk systems are allergic to restrictions on runtime changes. Loosely speaking, if I find something I can't change while my program is running, that restriction is a bug in my development environment.
Old systems like this will discourage certain kinds of changes because they're usually ill-advised, but will not forbid them, because forbidding them is anathema.
As an example, several Common Lisps implement package locks on certain system packages. A package lock prevents you from changing the definitions of system-defined constructs.
But it's Lisp, so it doesn't really prevent the change. It just makes it more inconvenient. You have to say "Mother, may I?" first, which gives you the opportunity to soberly consider whether making that specific change is really really what you want to do.
You don't connect (necessarily) the REPL to production. The code can be stateful code if you're not careless. Just like you oughtn't log into the production database server and write/update/delete data entries directly.
This is about the development phase more than the production phase. Do something like this (directly in the REPL or in a buffer as mikelevin has described doing elsewhere):
(defparamater *db* (connect-to-test-db))
(query-db *db* (make-query some-query-description))
;; see that it pulls out the desired record
(let ((record (query-db *db* (make-query sqd)))
(update-record record)
(write-to-db *db* record)) ;; update the entry
;; rerun the query above to see that things changed as expected
;; rewrite that let as a defun:
(defun make-update-to-some-record (sqd db)
(let ((record ...))
...))
;; test that it works
(make-update-to-some-record sqd *db*)
;; see that it does in fact do the same as the let above.
;; move that to a permanent source file, and build it
;; into the system.
;; move tests into test file.
;; repeat with next feature/task
The functions run in the REPL are stateful, but we aren't careless about what they're touching.
NB: You can connect to production environments. You can even connect to live, production lisp images. This could be useful for: profiling real-world activity, debugging real-world problems, applying hot-fixes. But especially that last one should be done carefully, and ought to have been validated using a test environment first.
Right. I usually start with creating global objects in the repl. Write little snippets in the repl to operate on these. The snippets end up with all these objects parameterized as functions. Which I can test again from the repl itself.
Turn the testing code into unit test as I go along.