You start a fresh REPL session with a vague idea of something you want to build. You have good tooling that enables you to write code in your favourite editor and pipe it to the REPL through a socket. You write a couple of functions and send them to the REPL. You call them a few times with some test arguments, maybe define a couple global variables while testing, and realize one of the functions doesn't quite behave the way you want. You tweak the code and send it again to the REPL. The function is redefined and works properly now.
You're happy with what you have for now, so you save the entire current environment of the REPL, functions and global variables and all, into a file (called an image) on your disk. The next day, you come up with a great new feature so you load up the image and continue exactly where you left off yesterday, with all your functions and even global variables as they were defined previously. You add your new feature, all while testing and tweaking your functions interactively, and then you save the new environment into an updated image. Any other code that loads that image will be able to take advantage of your updated functions and variables now.
The power of lisp is not just the fact that there's a REPL — a lot of languages have that now. It's the ability to save and reload the entire current state of the REPL (interactively or programmatically), which enables the powerful interactive development that so many lispers, including myself, rave about.
What happens when you want to share the thing you built with somebody else? If I type a defun into a REPL, is there a good way to get source code back out again?
Because there's a problem I've run into several times when writing Lisp (I use SLIME): I decide to refactor some stuff, so I rewrite some stuff, maybe remove a function or two from the source as part of that. Things seem to be working fine. I save my .lisp files and then at some point end up restarting the Lisp process (often because McCLIM can be kind of flaky and lose its connection to the X server). When I go to load my code back in, I get errors, because oops I was actually still calling that deleted function somewhere -- it "worked" before because the old function was still hanging out in the image.
When I develop in a compiled language, I just accept that I'm going to be re-building the entire thing from scratch (equivalent to restarting the lisp process & re-loading the source files) every time I want to try a change; if I broke something, I'll find out the next time I run make/go build/whatever. When I'm developing in Lisp, and I'll freely accept that my Lisp dev flow probably isn't ideal, it's so easy to screw up but be unaware of it for hours if not days: I write some code, I'm happy, I close Emacs, then a week later I come back and try to load my package and it's just broken.
I could solve it for myself by just using images: the "deleted" function would persist in the image, and every time I started working again it would be there. But if I ever want to let somebody look at the code, hack on it, make changes, what's that look like?
> If I type a defun into a REPL, is there a good way to get source code back out again?
this used to be the normal way of programming in lisp repls (like in 01980) but maybe this is good enough
* (defun tri (n) (loop for i from 1 to n summing n))
TRI
* (describe 'tri)
COMMON-LISP-USER::TRI
[symbol]
TRI names a compiled function:
Lambda-list: (N)
Derived type: (FUNCTION (T) (VALUES NUMBER &OPTIONAL))
Source form:
(LAMBDA (N)
(BLOCK TRI
(LOOP FOR I FROM 1 TO N
SUMMING N)))
> When I go to load my code back in, I get errors, because oops I was actually still calling that deleted function somewhere -- it "worked" before because the old function was still hanging out in the image.
so a thing you can do in that case is to delete it from the image at the same time
well, you can't really delete it from the image, but you can remove the function binding of the symbol, and since normally in common lisp implementations function calls indirect through the symbol (which is why you can replace the definition with a newer one) deleting the function binding is enough
* (defun tri0 (n) (tri (1+ n)))
TRI0
* (tri0 4)
25
* (tri0 5)
36
* (fmakunbound 'tri)
TRI
* (tri0 5)
debugger invoked on a UNDEFINED-FUNCTION @52A00674 in thread
#<THREAD "main thread" RUNNING {1001348003}>:
The function COMMON-LISP-USER::TRI is undefined.
Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL.
restarts (invokable by number or by possibly-abbreviated name):
0: [CONTINUE ] Retry calling TRI.
1: [USE-VALUE ] Call specified function.
2: [RETURN-VALUE ] Return specified values.
3: [RETURN-NOTHING] Return zero values.
4: [ABORT ] Exit debugger, returning to top level.
("undefined function" 6)
0]
as rainer pointed out, slime has a command for this (i barely use cl myself, you know much more than i do)
I work in a slightly different way by always saving my code in emacs and loading it fresh each session (though I have gone a couple of weeks with a live image/session on my laptop). I am fortunate that my 3D system [0] compiles and loads in a few seconds.
I also use the REPL for quick tests, and evaluate new code by writing it in emacs and compiling it one function at a time (C-c C-c).
Finally, I keep files of tutorial/demo/test cases which work a bit like a notebook, allowing me to quickly verify that existing 3D scenes still work.
About 15 or 20 years ago I had a work flow using SBCL Common Lisp where I saved images and always restarted a new work session where I left off. Easy to do.
I was motivated to do this because I had a ton of linguistic and text data in my environment which back then took a little while to load. Now, I organize everything I do into small or sometimes tiny Quicklisp projects/libraries and that is much more convenient for me.
If you want to really get into image based programming, the open source Pharo Smalltalk is very good.
This is the kind of thing that should be included in language tutorials!
It particularly resonates with me because I think so many project teams neglect the Developer Experience. In my opinion DevOps should be more about making coding, testing and deploying as frictionless as possible.
That is very helpful. Thank you. Most of my exposure to Lisp has been Scheme and Racket and minimal at that. Are these images you describe something that is part of a particular distribution of Common Lisp?
Images are pretty routine amongst most of them (though not all).
In the end what you have is a base image, and then a set of code and instructions to transform that image into your runtime image, then you distribute that image as your "application".
I have never needed to do this, as all of my work has been utilitarian in nature, rather than some kind of monolith distributed to others. I simply load my code on to the stock image and go from there.
But even then, for folks that do that, they may well load up the stock image with useful and common, personal, utilities, and snapshot that. Their "application" is their essential working environment.
Smalltalk does, and Forth can work from this concept of an image.
Schemes tend to not be this way. They are interactive, but the image concept is not as ingrained in the Scheme world. Instead, it's more like doing development in something like Bash -- an interactive environment to be sure, but one that always resets to the baseline state and must be reloaded when started. There are certainly Schemes that can do this, it's just not as routine.
You start a fresh REPL session with a vague idea of something you want to build. You have good tooling that enables you to write code in your favourite editor and pipe it to the REPL through a socket. You write a couple of functions and send them to the REPL. You call them a few times with some test arguments, maybe define a couple global variables while testing, and realize one of the functions doesn't quite behave the way you want. You tweak the code and send it again to the REPL. The function is redefined and works properly now.
You're happy with what you have for now, so you save the entire current environment of the REPL, functions and global variables and all, into a file (called an image) on your disk. The next day, you come up with a great new feature so you load up the image and continue exactly where you left off yesterday, with all your functions and even global variables as they were defined previously. You add your new feature, all while testing and tweaking your functions interactively, and then you save the new environment into an updated image. Any other code that loads that image will be able to take advantage of your updated functions and variables now.
The power of lisp is not just the fact that there's a REPL — a lot of languages have that now. It's the ability to save and reload the entire current state of the REPL (interactively or programmatically), which enables the powerful interactive development that so many lispers, including myself, rave about.