I frequently hear about how amazing the experience of coding interactively with lisp is. I learned a bit of lisp (at the level of Practical Common Lisp) and scheme (via SICP) once upon a time, but I never coded interactively. Is there a way to demonstrate the power and utility to someone like me? Perhaps there are good videos or streams or materials specifically on or using the interactive development?
I'm a bit fond of the short 5-part series here https://malisper.me/debugging-lisp-part-1-recompilation/ that covers some of the debugging aspects, it's got some animated gifs. But there are several screencasts referenced on https://lisp-journey.gitlab.io/resources/ that may be worth exploring, though the fastest method is probably to just try building something yourself with the constraint that you never restart your Lisp image. Not too long ago I ported an old 2D maze generator I made in pygame to CL with SDL2, then while I had it up I decided I should add a solver to it, so ported an old A-star implementation I wrote in C++ and hooked up drawing (with dots) a path from any point to any other point. Then I wanted to draw a nicer sprite path that connected and handled corners by curving, so I swapped those in and then one-by-one recompiled the code with new ifs to handle each corner case to select the right path tile. Having to close and relaunch and reset the test points (and save and restore the maze seed) every time I made a code change would have been rather tedious.
If you've worked in Java, download the JRebel extension for more complete hot-reloading support, and that'll give you an approximation of the interactive development experience. It's especially useful for web apps where you can change the code for an endpoint (or used by several endpoints, whatever) and can then just test it out with new requests, no need to restart the server.
That first link is the one I was trying to recall. It's a good introduction to it.
I had some fun doing "debugger driven development" with Lisp for the Synacor Challenge. Basically, you have to implement an interpreter/VM and there are some number of instructions (around 20?). Instead of trying to correctly implement all of them from the start, I had a core loop like this (not exactly, summarized):
(loop with pc = 0
for instruction = (aref memory pc)
do (ecase instruction
))
ecase will signal an error whenever there is no matching case, taking you to the debugger. I read what instruction caused the error, checked the spec, implemented it, and resumed the program. Throw in a few asserts as sanity checks to complete the process. I don't recall the instruction set, but suppose that "add a b c" means add the values in registers a and b and store in c, then maybe I'd do something like this:
(10 ;; or whatever opcode meant ADD
(... logic for addition ...)
(assert (= c (+ a b))) ;; with whatever was needed to obtain these values correctly
If I implemented something incorrectly, I just rewrote the logic, recompiled, and restarted again. It was an enjoyable way to build the system, but of course it also relied on having a fairly comprehensive notion of what the end result would be and I did have to refactor at the end. But I had some good test cases by that point so refactoring wasn't bad.
I did the same thing for another project where I had a variety of input files that exercised various parts of the system and just continuously iterated until I got the expected result for an input relying on the debugger (triggered by ecase and assert and (error 'unimplemented)) to detect where I hadn't implemented something or had implemented it incorrectly or incompletely (like maybe one input exercises a particular edge case that others didn't). Once a file was properly processed, I wrote that up as a proper test, and moved on to the next file. Which then made the refactoring easy because I had a comprehensive test suite.
I wonder if Neovim wouldn't be an even better option for this. Isn't slimv written in that dreadful extension language for Vim? I may have seen it years ago and don't remember anymore.
There are two wrapper scripts, for Terminal (`ritt`) and for iTerm (`riitt`), in addition to the `swank-lisp` script. Choose one of the wrapper scripts for your use case.
The relevant lines in ~/.vimrc, adjust your `sbcl` path:
let g:slimv_swank_cmd = '!ritt swank-lisp -d "' . getcwd() . '"'
let g:slimv_lisp = '/usr/local/bin/sbcl'
let g:slimv_impl = 'sbcl'
let g:slimv_ctags = 'ctags'
`ritt` (“run in Terminal tab”) for use with Terminal.app:
#!/usr/bin/env osascript
on run arglist
if (count of arglist) > 0 then
-- The command to run is built from blank-separated script arguments.
set text item delimiters to {" "}
set theCommand to arglist as string
tell application "Terminal"
activate
-- Make a new Terminal tab just like the current one.
tell application "System Events" to keystroke "t" using {command down}
set newTab to the last tab of front window
-- Run the command in the new tab.
do script theCommand in newTab
end tell
end if
-- Try to cause a minimum of disturbance in the calling environment instead of outputting random AppleScript values.
return ""
end
This is `riitt` (“run in iTerm tab”) for use with iTerm.app:
#!/usr/bin/env osascript
on run arglist
if (count of arglist) > 0 then
-- The command to run is built from blank-separated script arguments.
set text item delimiters to {" "}
set theCommand to arglist as string
tell application "iTerm"
activate
-- Make a new Terminal tab just like the current one.
tell application "System Events" to keystroke "t" using {command down}
set newTab to the last tab of front window
-- Run the command in the new tab.
write (current session in newTab) text theCommand
end tell
end if
-- Try to cause a minimum of disturbance in the calling environment instead of outputting random AppleScript values.
return ""
end
And here the `swank-lisp` script (adjust the `swankdir` to your install location):
#!/bin/bash
swankdir="${HOME}/.vim/bundle/slimv.git/slime"
self=$(basename "$0")
umask 022
set -u
Usage()
{
cat <<EOD >&2
Usage: $self [-d <working-dir>]
EOD
exit 1
}
# Argument Scanning
if [ "$*" = "--help" ]
then
Usage
fi
devDir="${HOME}/Develop/Lisp"
while getopts "d:X" opt
do
case "$opt" in
(d)
if [ -n "$OPTARG" ]
then
devDir="$OPTARG"
fi
;;
(X)
set -x
;;
(?) # Unknown option
Usage
;;
esac
done
# Shift away parsed option flags
shift $((OPTIND - 1))
# Change to user's development directory, so files are easier to locate.
cd "$devDir" || exit $?
sbcl --dynamic-space-size 16384 --load "${swankdir}/start-swank.lisp"
echo # Newline before next shell prompt.
My manager at my last job was a real wizard with Vim+Common Lisp, he was much more facile than I am with Emacs+CL.
To be honest, when I am reading other people’s Common Lisp code, if there are many files in their projects, I prefer VSCode. And for my own hacking joy I like LispWorks, usually using the IDE, but sometimes using Emacs.
From an IDE point of view, I reckon it's hard to tell for a Emacs user, but the sure thing is that LispWorks the editor has more graphical widgets. The graphical stepper for example is for sure easier to use and discover than the rest. https://lispcookbook.github.io/cl-cookbook/lispworks.html LispWorks the implementation has a bunch of features.
Me too. The primary use case for Emacs is writing Emacs init files[1], which are themselves a dialect of Lisp. Because of this, it's a solid tool out of the box for working with s-expressions. On the other hand, The vi family is historically very much a line oriented editor, so I'd be very interested in seeing a screencast of what an optimized vim CL workflow looks like.
On a tangent, I feel the popularity of vi style bindings is more of an indictment of the standard substandard terminal based keyboards we use. Emacs bindings make a lot more sense ergonomically on a space cadet style keyboard where control and meta are comfortably pressed with the thumb.
I've been really happy with slimv. There are a few things missing that I'd like to have were I to find myself working on a really large program with a bunch of other programmers, though I suspect the commercial Lisps offer a good approximation. Besides the trivial things like more auto-refactoring tools (thanks to cross-referencing I can at least get a list of all the locations something is used and jump to edit them one by one if necessary) and project organization tools (I've started using @export from https://github.com/m2ym/cl-annot rather than going back to my package definition to keep adding symbols to the export list) I'd like a better line debugger. It hasn't been a hurdle so far because what's there is good enough (as the article describes, when you hit the debugger you get your stack, you can inspect stuff in the frame, you can recompile and then restart computation from a frame instead of aborting the whole thing). If your declaim settings are right you can also step your code and so on within vim but it's kind of clunky, I'd rather launch a dedicated GUI that's at least as nice as the old Insight GDB wrapper. When inspecting complex data I've started to use the McCLIM app Clouseau: https://github.com/McCLIM/McCLIM/tree/master/Apps/Clouseau I bound ,ci to call (clouseau:inspect) on the symbol and that launches a nice enough GUI to explore it. (Repeated periodically in a thread also serves as a poor man's variable watcher...) A handful of other vim plugins make the full experience even better (even when not writing lisp).
It's been pretty amusing watching the LSP landscape evolve for other languages, it's almost like swank for CL. But it's rather nice to have the server be embedded in the process itself. On my personal web server I have a compiled lisp binary running, but I shipped it with swank listening on a local port, so if I want to change something without rebuilding and redeploying I can just SSH in while forwarding port 4005, connect to the lisp image with my local editor, and recompile functions or whatever. At my last job I also inserted ABCL into the huge Java app on my dev box and had it start a swank server, letting me connect with vim and mess around -- it was mostly useful for quickly launching system tests which otherwise had a dedicated clunky browser UI, and writing some code to quickly extract or insert data. I had some designs to write some webdriver tests in Lisp and demo how the debugging experience when one fails can be much better (not having to restart the whole flow because a UI element changed its class name or whatever and threw an exception) so as to introduce the language to the broader company officially (and hopefully push for officially sanctioned use), but never got around to it before I left.