On Repl-Driven Programming (2020)
mikelevins.github.io
mikelevins.github.io
You may remember the rich comment forms in Clojure development as a communication tool to your future self and others. Now someone made them testable https://github.com/hyperfiddle/rcf#readme I feel this is even closer to the platonic ideal Clojure way of serializing your thoughts to file.
All that said I’ve never really liked this idea of editing as you go with extreme late binding in the debugger. Just seems a recipe for non-reproducible code. I love being able to explore and experiment in the REPL, but ultimately everything coalesces into code and tests in files. These give me far more confidence to move forward than a long lived REPL session.
There’s stuff like Reveal[0] which is interesting but I’ve only ever found it a marginal upgrade on pretty printing, at the cost that it sits outside Emacs so slows me down anyway.
On Repl-Driven Programming - https://news.ycombinator.com/item?id=25620256 - Jan 2021 (206 comments)
Not so sure about swift playground.
One surprise may be mvs … it is not a Repl. But the idea is that the system cannot be stopped. The sysplex cannot be down because you have a sub-system or just a program error. You have to look at any core dump and fix the system whilst it is running. Obviously not development-Repl. Waterfall and punch card, thank you. But production-Repl.
For production-Repl, basically all current systems needed it. There is no stop of your game world. Or may be yours but not the underlying world. It continues to run even on function upgrade or bugs etc.
The real question is this production-Repl kind (not exactly Repl but the feature of keep the image running, debug or change one part of it and propagate the change without breaking the system) is really hard.
Changing a s/370 control block or jes2 exit lately? Good luck and thanks you for all the fishes, that is why I go away.
If someone did that, they'd be a fool. Common Lisp and Smalltalk, the quintessential REPL-driven languages, have supported storing and loading source from files since their standards were published, and before then since the standards weren't created out of thin air.
In other words it was a cultural thing, not a property of the languages. And even with the systems for which that was possible it was also possible to start with a fresh band and load code into it.
The notion that you'll lose your work if you use the normal means of interacting with the repl is a misunderstanding of how such systems work.
I've been doing repl-driven development with Common Lisp compilers for about 33 years now. I have never ever worked in the way that you describe, and have never witnessed anyone else working that way, either.
It would be a nightmare for maintainability if anyone worked that way, but if they do, I haven't ever seen it.
I do write the state of the runtime to image files from time to time for various purposes. I assure you, though, that all the code I write lives in files that are maintained under revision control, and it's been that way for many years, and in all that time I've never been in danger of losing any work because of forgetting to write an image file.
Nevertheless, this still does not add up to precisely what the GP described.
Smalltalk in contrast has GP's model of saving and reloading the VM state.
Interlisp and most Smalltalk systems also offer the convenience of automatic change tracking, writing your changes to the sources file and the changes file, and providing in-image tools for browsing and managing changes, including things like Interlisp’s Masterscope and Pharo and Squeak’s Monticello. These tools make life more convenient for people who prefer to work mainly with the in-image tools and not to mess with text files that much.
But if there’s a Smalltalk system that relies solely on the image file to preserve changes, with no support for other means of change tracking, I’m not aware of it.
Part of the confusion on this point is thinking that the prompt is the repl. It isn't. It's a UI for the repl. The repl is the process that reads, evaluates, and prints, then loops back to do it again.
Nothing about a repl requires there to be a prompt, and not all UIs for repls have them. Interlisp and Smalltalk have relied mainly on promptless UIs for their repls since the 1970s.
When working with Common Lisp, the main UI I use is an Emacs editor buffer and the key bindings that send input directly to the repl (bypassing the prompt). Typing at the prompt is the exception rather than the rule, and I could get along fine if I didn't bother to load the slime-repl contrib that provides it.
It is more than four decades old obvious basic idea, and I do not understand why almost everybody ignores it.
Not an expert, but your changes never 'leave' your source files. You iterate by changing the code in the file, sending it to the image/compiler, and repeat. There's no need for "getting code back into the files".
I send my changes to the repl with the stroke of a key.
Working with a repl doesn't necessarily mean typing text at a prompt. The prompt isn't the repl; it's just a particular UI for the repl.
Smalltalk systems do tend to store edits in the memory image (though they also write them automatically to a changes file and a sources file, so they aren't lost if you blow away your working image). Smalltalkers generally prefer to work in the image and treat text files as a serialization format for exchanging code and data.
But normally there's no question of getting "changes back in your source file" because they were always in your source file, or its equivalent, from the start.
The old ways —
"Within each project, a set of changes you make to class descriptions is maintained. … Using a browser view of this set of changes, you can find out what you have been doing. Also, you can use the set of changes to create an external file containing descriptions of the modifications you have made to the system so that you can share your work with other users.
…
The storage of changes in the Smalltalk-80 system takes two forms: an internal form as a set of changes (actually a set of objects describing changes), and an external form as a file on which your actions are logged while you are working (in the form of executable expressions or expressions that can be filed into a system). … All the information stored in the internal change set is also written onto the changes file."
1984 Smalltalk-80 The Interactive Programming Environment page 461
https://rmod-files.lille.inria.fr/FreeBooks/TheInteractivePr...
The somewhat less old way —
https://www.google.com/books/edition/Mastering_ENVY_Develope...
Even under Smalltalk, which works more like you say, the system tracks and saves source changes. At least that's how it works under Squeak and Pharo.
Very few developers would be foolish enough to use a heap image as the source of all truth. I'm sure it happens, just like the place I worked at whose entire web stack was based on a magic DLL no one knew how to compile, happened. Like the magic DLL, it's widely considered a terrible, terrible idea.
The workflow is amazing because you write the software in the editor you're used to and have keybindings for interacting with the REPL inside your editor.
In this case, there's a claim that python isn't actually REPL-based because you cannot change state while in an exception handler. Yes, that's kinda true, but there are debuggers for python that if you really need to can do a lot of that.
Other notable examples of this are type system maximalists. E.g. Go: no sum types or generics (until recently). Rust: no HKTs. Python: no runtime checks (I actually sometimes think this :)).
Sweeeet.
Well damn.. here I am in my python repl for hours per day, feeling smug about how efficient I am pulling logs and analyzing data. But is there an AWS lisp sdk?
Edit: there is but not officially.
Sounds fun but lisp seems so much less interoperable (out of the box) than python, and more of a departure from the style of everyday engineering. I love my Python repl tools.
ClojureScript (shadow-cljs, nbb) for JavaScript
Clojure for JVM
babashka for scripts
libpython-clj for python interop.
ClojureCLR for .NET (beta state IMO)
ClojureDart for Dart/Flutter (early alpha state, one big shipped prod app on iOS/android)
--- Edit:
I don't quite know how to describe it, but having the _exact_ same files in a full-stack web application being read in both the Clojure backend and ClojureScript frontend (API contract) feels pretty neat. As an example having the frontend accurately give the user feedback why his input form isn't going to be accepted without a server round-trip or duplicating code just feels elegant.
anyway, check out
https://www.youtube.com/user/CBaggers/videos
https://github.com/rabbibotton/clog/blob/main/LEARN.md
edit: https://malisper.me/category/debugging-common-lisp/ (from another poster)
void foo() {
bar();
}
in those languages and expect the compiler to be thrilled about it. But you could get closer. Java runs on the JVM, C# on the CLR, those provide systems on which the languages (or programs written in those languages) are executed and the kind of access needed to approach the debugger-driven development approach. I haven't done this in a long time but with C# you can do: void foo() {
throw new NotImplementedException();
}
Now when your program execution hits `foo` you can catch it in the debugger (versus just crashing or otherwise terminating, or needing to set a breakpoint). With VS (and probably others, I did this with VS) you can change this code and recompile it and resume from that point. Now you decide to make the call to `bar`, oops, that method doesn't exist yet so you also have to create `bar`, and you can stub it out the exact same way. So it's not as easy and pleasant as in Lisp or Smalltalk where you can forgo the skeletal framework of the program and jump straight into it, building each function, method, class, etc. only as needed (truly JIT development), but you can get close. I believe Java has some similar facilities with some dev environments. There is more work, but it's not totally impossible.All that said, I really enjoy the way you can do this in Lisp (and Smalltalk). I've been debating re-solving the Synacor Challenge [0] and recording how I did it in Lisp the first time. I used the debugger in precisely this way, writing a basic version which could read in the binary blob and did minimal parsing, but then entered an ecase (case expression that errors if there is no matching case). For every instruction that I hadn't implemented (none of them at the start) it brought me to the debugger, I modified the program, resumed it and it continued until the next unimplemented instruction. By the time I'd finished, I had the interpreter for the byte code up and running and was immediately getting to work on the rest of the challenge. This also worked as a nice goal-directed approach, implementing the whole interpreter was a "big" (not really) task, how to break it down and which part to do first? Well, interpreting the binary blob acted as my (very complex) test case and gave me immediate feedback (I also added some basic debugger capabilities to the interpreter) on whether I had done it correctly. No need to make the whole thing before testing it, I got to incrementally test each small change.
That approach could also be made to work with C# as I described before, write a basic skeleton and parse the instructions but throw a `NotImplementedException` whenever you get to an instruction you haven't, well, implemented yet. Then using VS change the throw to a method call (or do it locally) that implements the handler for that instruction. Repeat until all of them are filled out. It's not as smooth as the Lisp or Smalltalk cases would be, but it's doable.
[0] https://challenge.synacor.com - programming challenge by the same guy that does Advent of Code.
----------
Also, this has come up on here a few times in the past since it was written (2020). Two past submissions with comments:
https://news.ycombinator.com/item?id=25620256 - Jan 3, 2021 (208 comments)
https://news.ycombinator.com/item?id=25618457 - Jan 3, 2021 (5 comments, 4 + 1 dead, don't know why it's dead)
Seems like I should try Smalltalk
Console.WriteLine("Hello");
For REPLs, there are options like (my own) https://github.com/waf/CSharpRepl which stand on top of the Roslyn compiler infrastructure, which is quite extensive and can easily evaluate standalone functions and statements.It's still nowhere close to the REPLs of lisp and smalltalk, but it's a step in a more flexible direction.
Smalltalk has equivalent features.
Lisp won't catch every possible error you could possibly make, of course, so testing is good defensive programming.
On the other hand, when I'm working in Lisp, testing happens almost by default. It's the majority of what I do: I write experiments in files and hit a key to send them to the repl to test what happens.
Tests generally start as little experiments like this. Some of them are useful over and over. Those graduate from informal experiments to formal tests.
By default, any slots that have been added will be unbound. Attempting to access them will raise an error which will drop you in the debugger. From there you can then fix the instance and continue running.
Actually typing text in the repl buffer is very much the exception, not the rule.
LISP and Smalltalk have the only True REPLs because only in those can you rebuild the interpreter and redefine classes without restarting. Everything else is just so cute with how hard they try.
I don't actually think that Lisp and Smalltalk systems are the only possible examples of comprehensively interactive development systems. I'd say FORTH systems qualify (though they lack a lot of creature comforts) and so does Factor. Probably some others I'm not thinking of, as well.
I also don't think it's impossible to build a proper repl-driven development environment with other languages. I just think that the needed work hasn't been done for very many other languages, and for perfectly understandable reasons.
One reason is that it's a lot of extra work to build a proper repl-driven environment. I mean, you have to do all the work that would be needed for a non-repl-driven environment, and then you have a bunch more work to do on top of that.
A second reason is that in order to build such an environment you have to be sufficiently familiar with them to know why you would want one, and then you also need to be the sort of programmer who actually prefers to work that way (or else why would you bother with all that extra work?).
And, finally, you have to have the time, ambition, and other resources needed to actually build the thing.
So it should be no surprise that proper repl-driven environments are the exception rather than the rule, but I still prefer them, so I want more people to know about them, in hopes that some fraction of people will find them preferable, as I do, and the demand for them will grow, and that demand will help ensure that they don't disappear.
They're how I prefer to work. I don't want to lose them.
Once you've worked that way, the regular way of programming seems archaic and a pita.
check out CLOG - The Common Lisp Omnificent GUI framework for building web apps in common lisp
The methodology described in the posting does exist, but it is never used for development; it is, however, very commonly used in three settings: (1) one-shot short programming [1SP], (2) exploratory programming [XP], and (3) interactive debugging [ID].
I just last night did an examples of 1SP: I was trying to work out the equation for a combinatorial probability, and wanted to simulate it so that I could tell what the right answer should be. I wrote the simulation expression right into the REPL -- not even in EMACS (my usual mode of programming is emacs+shell(usually lisp) -- I'm so old-timey that I never even bothered to install SLIME!) It took a couple copy-paste fixes to get it to work, but in a couple minutes, I had my result.
I do XP all the time, but usually on a single function, or small set of them, often when I'm trying to make a complex recursion work. This is like 1SP, but I'm not going to throw away the code; to the contrary, I'm going to copy the final, hopefully working, code into the body of my program. Therefore, I'll often do XP in an emacs+shell(lisp).
ID needs no further explanation; this is well described in the posting, and is, as mentioned, quite a wonderful process. (Sometimes ID and XP are mixed, as suggested in the posting.)
What never even happens is complete software development in lisp "core" -- what's sometimes called "workspace" development. This can be done, but it's a terrible software engineering methodology. (APL, which I've also done quite a lot of, although not so much anymore, has had a REPL since around the same time as Lisp, and has always worked in the workspace parading, and it was always a terrible mess for anything very complex.) [BASIC also had a workspace paradigm and REPL since around the same time as Lisp, but no one did anything big in the early BASICs, so this was fine. They were mostly used by student on early timesharing systems.]
One very important and under-appreciated property of Lisp that really makes interactive programming work so well in Lisp, and not so well in most other languages, is that line turns and white space make no difference at all to Lisp; it's all from the first open paren to the last close, so you can copy-paste whole expressions into the place of variables, if you like (more commonly the other way around, I guess), and there's never any question about it being syntactically correct.
For example, I take this expression:
(foo (bar (baz ...)) (bar (baz ...)))
where, say, (bar (baz ...)) is a heavy computation, so I want to refactor it. All I need to do is pull the expression (bar (baz ...)) out using paren-matching, wrap the thing in a let, and bob's your uncle, as:
(let ((x (bar (baz ...)))) (foo x x))
But the thing to note it that the expression (bar (baz ...)) could be 50 lines long! As I said above, line turns and white space (aside from enough white space to break up obvious things, like the names of separate variables), are mere decoration to Lisp, so you can decorate, or not, all you want.
This facility to not care at all about the lines and spacing, which most other languages care about in one or another way; make interactive programming a real joy.