On repl-driven programming
Dec 18, 2020Once upon a time, someone with the handle “entha_saava” posted this question on Hacker News:
Can someone knowledgeable explain how are lisp REPLs different from Python / Ruby REPLs? What is the differentiating point of REPL driven development?
The answer is that there is a particular kind of programming in which you build a program by interacting with it as it runs, and there are certain languages and runtimes that are designed from the ground up to support that kind of programming.
Python and Ruby are not examples of such languages.
Why not? That’s the crux of entha_saava’s question, right? What are these repl-driven programming systems, and what makes them different from Python and Ruby and every other language that offers a repl?
For that matter, what’s a repl?
The word repl is an acronym that stands for read-eval-print loop. The term comes from the history of Lisp. From the start, sixty years ago, the standard way of working with a Lisp has been to start a language processor, type expressions at its prompt, and wait for it to evaluate the expressions and print their results, before prompting for another expression. Read, eval, print. Loop.
Nowadays, repls are all the rage. Every language and its brother offers a repl. There’s a website, repl.it, whose entire purpose is to provide all the repls.
It doesn’t actually provide all the repls, of course. What’s particularly ironic is that it doesn’t provide either of the canonical repl-driven development environments: Common Lisp and Smalltalk.
That brings us back to entha_saava’s question: if Common Lisp and Smalltalk are repl-driven environments, and Python and Ruby are not, what’s the difference? What do Lisp and Smalltalk have that Python and Ruby don’t?
What they have is a language and runtime system that are designed from the ground up with the assumption that you’re going to develop programs by starting the language engine and talking to it, teaching it how to be your program interactively, by changing it while it runs.
I can hear the objections formulating already. I’ve seen them before. Yes, every language with a repl can do some things in the repl. Obviously that’s true; if it weren’t, then the repl would be entirely useless.
Being able to do some things in the repl does not make an engine into a repl-driven programming environment. What distinguishes old-fashioned Lisp and Smalltalk environments is that you can do everything in the repl. They place no gratuitous limitations on what you can do; if the language and runtime can do it, then the repl can do it.
For example, you can ask the current version of Clozure Common Lisp to rebuild itself from scratch by evaluating the following expression at the repl prompt:
(rebuild-ccl :full t)
CCL responds by completely rebuilding itself from source.
The point is not that you would want to rebuild CCL this way all the time. The point is that there are no artificial limitations on what the repl can do. The full range of the development system’s capabilities is accessible from the repl.
That’s one of the first things I notice when using newer, lesser repls: I’m always running into things I can’t do from the repl.
It’s not just about freedom from restrictions, though. Proper support for interactive programming means that the language and its runtime have positive features that support changing your program while it runs.
Try this in your favorite repl:
Define a function, foo
, that calls some other function, bar
, that
is not yet defined. Now call foo
. What happens?
Obviously, the call to foo
breaks, because bar
is not defined. But
what happens when it breaks? What happens next?
If your favorite repl is Python’s or Ruby’s or any of a few dozen other modern repls, the answer is most likely that it prints an error message and returns to its prompt. In some cases, perhaps it crashes.
So what’s my point, right? What else could it do?
The answer to that question is the “differentiating point” of
repl-driven programming. In an old-fashioned Lisp or Smalltalk
environment, the break in foo
drops you into a breakloop.
A breakloop is a full-featured repl, complete with all of the tools of the main repl, but it exists inside the dynamic environment of the broken function. From the breakloop you can roam up and down the suspended call stack, examining all variables that are lexically visible from each stack frame. In fact, you can inspect all live data in the running program.
What’s more, you can edit all live data in the program. If you think that a break was caused by a wrong value in some particular variable or field, you can interactively change it and resume the suspended function. If it now works correctly, then congratulations; you found the problem!
Moreover, because the entire language and development system are
available, unrestricted, in the repl, you can define the missing
function bar
, resume foo
, and get a sensible result.
In fact, there’s a style of programming, well known in Lisp and Smalltalk circles, in which you define a toplevel function with calls to other functions that don’t yet exist, and then define those functions as you go in the resulting breakloops. It’s a fast way to implement a procedure when you already know how it should work.
If you’re a user of old-fashioned Lisp or Smalltalk systems then this all sounds obvious to you, but that reaction is not common. Surprise is much more common, or even suspicion: what’s the catch?
The catch is that the designers of your language system had to think that facility through in the planning stages. You don’t get a decent implementation of it by bolting it on after the fact. Breakloops need full access to the entire development system, interactively, with a computation and its call stack suspended in the breakloop’s environment.
Let’s take another example of a facility designed to support interactive programming. Once again, try this in your favorite repl:
Define a datatype. I mean a class, a struct, a record type–whatever user-defined type your favorite language supports. Make some instances of it. Write some functions (or methods, or procedures, or whatever) to operate on them.
Now change the definition of the type. What happens?
Does your language runtime notice that the definition of the type has changed? Does it realize that the existing instances have a new definition? When something touches one of them, does it automatically reinitialize it to conform to the new definition, or, if it doesn’t know how to do that, does it start a breakloop and ask you what to do about it?
If the answer is “yes,” then you’re probably using a Lisp or Smalltalk system. If the answer is “no,” then you’re missing a crucial element of repl-driven development.
Remember: the point is to support programming interactively. You don’t want to have to kill your program and rebuild it from scratch just because you changed a definition. That’s silly; adding and changing definitions is most of what you do! If your development environment is going to support interactive development, then it had better know how to keep your program running when you change some definitions.
Old-fashioned Lisp and Smalltalk system know how to do that. There are also a few other kinds of systems, mostly older ones, that know how to do it.
These are not eccentric new ideas out of left field. They’ve been around for half a century. They contribute materially to productivity in interactive development.
They’re what sets repl-driven development apart from mere development with a repl.
Now, not every programmer prefers that kind of development. Some programmers prefer to think of development as a process of designing, planning, making blueprints, and assembling parts on a workbench. There’s nothing wrong with that. Indeed, a multibillion-dollar international industry has been built upon it.
But if you prefer interactive development, if it’s more natural to you, then it can make you enormously more productive, not to mention happier in your work.
Interactive development with a proper repl-driven environment is the exception. Most programming is done in other ways.
As a consequence, there are a lot of programmers out there who’ve never even heard of it, who have no idea that it exists. My intuition is that some fraction of those programmers would prefer well-supported interactive programming, and would benefit from it, if they just knew what it was.
Maybe if enough programmers are exposed to that style of programming then we’ll begin to see new tools that embrace it.