Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I have a concern about REPL driven development, which is around how maintainable the result is.

The reason people like REPLs is because they can experiment to discover the right way to write the code in a very fast feedback loop. This alleviates a huge pain when you are operating with uncertainties or at the edge of your knowledge - 'does this function return null if no matching entries or an empty list?' - etc.

The problem is that the reader of your code is not going on that journey with you. They are coming in naively without knowing all of that experimentation you did. The writer now has the opportunity to create code that "just happens" to work by a magic of coincidences and conveniences based on very subtle non-obvious properties of the APIs and systems they are working with. The reader is severely disadvantaged and will have a much harder time to reach parity with the state of knowledge that the writer had. For example, some property of a function or object may not be documented at all, but through the REPL the writer has ascertained it is true. How can the reader know this?

So its a double edged sword in that the more you empower the original creator of the code, the greater imbalance you create to the later readers and maintainers of the code. Used well and with great discipline I could see it being very powerful and positive force to make better code. But in the worst case scenario, you'll end up with extremely unmaintainable code.



I have worked on massive LISP projects that were developed using REPL-driven development, and your fears are critically valid. Even worse, this basically hamstrings it as a standalone application. The workflow to _use_ the application becomes "start the REPL under emacs and launch it", which is, in my personal opinion, a completely unacceptable distribution model. And if you don't use emacs, good luck to you! Once set up as REPL-based development, Smalltalk and LISP both have to essentially ship their "whole environment" to deploy the application. This only works out well for small projects that do not need to be distributed, or whose distribution you, the developer, have a lot of control over.

(There's an aside to be said about building binaries from a LISP compiler, but most binaries I have seen produced by such compilers still need access to the required libraries, which sort of hoses it as a distribution solution. This could be a result of the build system used on the projects I work on.)

And your documentation concern is valid, but I think even worse is the design concern: when designing an API, you have to stop and design it and consider the various concerns. If you develop it as you need it / as you go, it is possible to arrive at sub-optimal designs. Hacking the "next piece" out at each step is likely to lead to slanted designs with poor APIs, requiring future refactoring. You see this with programmers at every level: if you ask them to write a program to do a single thing, then add more (related) capabilities, and continue ad nauseum, at some point the program will need to be refactored. This suggests that, without prior planning, REPL-based design is set up to incur technical debt. Unfortunately, my real-life experience confirms this. Moreover, refactoring something in a REPL is... a chore, to say the least.


Your observations are valid, and need to be kept in mind when developing in an exploratory, interactive way.

Your alarm seems a little overblown. Maybe it reflects a particular bad experience? I've delivered a bunch of products built in the way I prefer to work, and it's been a long time since I've had the problems you're describing. Maybe that's because I ran into such such problems early in my career and learned ways of handling them.

And yeah, if you just iterate and explore, you can wind up in lacunae. You do need to develop some actual goal and a vision of how to get there. Interactive development can be really helpful for the exploring part, but it can't pick your goals for you.

In short, interactive development is a way of working that's good for some cases and some people (me, for instance). It's not a cure-all or an objectively superior methodology for all people or all cases. I don't want it to take over the world. I just want it to continue to be an option because it's the way I prefer to work.


Oh man, I appreciate your pragmatism and open-mindness. It is refreshing to see someone advocate for something without trying to "take over the world" and proving others wrong.

Thanks for sharing your experience amd positivity with the world.


Thanks for taking the time to respond. I wrote up my comment just to point out some of the weaknesses I have identified in REPL-based development, and how to watch out for those pitfalls as a program scales. For what it's worth, I agree with your sentiment, and believe REPL-based development should always be an option. I have a lot of success using REPL-based "prototyping", even in python, where I will quickly try/test something at a REPL before scaling it up, and having that utility to try/test/experiment quickly is often invaluable (especially if the standard build time is on the scale of minutes).


Echoing this, I'd really like it to be an option in other languages, because it's just a pleasant way to work and it's nice to have the option.


> Even worse, this basically hamstrings it as a standalone application. The workflow to _use_ the application becomes "start the REPL under emacs and launch it"

This seems like an incidental property (symptom of poor engineering discipline, which manifests itself in other ways in other development paradigms e.g. lack of documentation from "agile" teams) of the particular applications you've worked with and not an essential property of the (architecture decisions resulting from the) REPL-driven development style. What makes you think that this is actually due to REPL-driven development?

I develop several tools from the REPL and was able to easily convert each one to a standalone tool (when I attempted to do so).


The experience I have had on large, REPL-driven development is that continued development is assumed to also work under REPL-driven environments, so debugging or supporting them begins with "try typing X into your REPL." Many of the distributables I have seen also maintains REPL interactivity as part of the live deployment solution, with similar debugging solutions. While this may not be unilateral, "ship the deployment environment" seems to be a running theme.

I'm interested in your experience in avoiding this, and the process you used.


The batch-processing tools I wrote (e.g. a simulator) had CLI shims that called the same interesting functions that you would at the REPL. The interactive tools I wrote all had functions that acted as a zero-effort entry point anyway, so the conversion to standalone tool just consisted of building an image that called that function.

Additionally, my development environment (SLIME) talked with my application over a network socket, so there's no linkage between it and the application - at least, any more so than the usual problem of "Common Lisp binaries are hard to make small" but that's not specific to REPL-driven development.

The simplicity of my "solution" makes me think that we might be talking at different levels here, but I can't think of what the disconnect might be.


As stated before, the fundamental disconnect is that you are imagining a world where all subsequent development on your code is done via SLIME and sockets or some similar REPL instance. You see this as simple because you wouldn't consider developing without SLIME / sockets in the application, but consider the other side of the coin. If I am a developer who does not use SLIME / socket connections as my default development pattern, my first step to contributing to your codebase is to either (i) convert to this workflow and toolset, or (ii) develop all of the CLI shims you described, including maintaining them as functionality changes. That leaves me between a rock and a hard place, especially if my text editor of choice doesn't have good SLIME support.

Also, I do not quite buy the simplicity of the standalone tool conversion; it assumes the relevant functions are naturally well-suited as entry points, including handling malformed inputs, etc., very cleanly. In my experience, many things that make sense at a REPL in a live system need to change dramatically to mature into robust command-line tools.

As for deployment, the interactivity I am describing is exactly the linkage you mention, and it really does come down to shipping a development environment as part of the deployment environment. Shipping a binary with a swank server or similar introduces binary size issues, portability issues (you have to ship dependent libraries along with the binary), and some serious security implications. And while this may be the "lisp way", modern languages manage to avoid these issues just fine.


The author of the code is going to have to discover how these APIs work somehow, right? Why would the code end up worse if the author has access to a particular tool for interacting with these APIs? A shell and curl is a REPL for interacting with and exploring HTTP APIs but I don't think anybody would claim that the use of such tools makes code written to those APIs harder to follow.

In fact, one of the benefits of your REPL being integrated with one's editor, and being in the language of the system, is that the kinds of ad-hoc scripts you write when doing exploratory / debug coding which one would typically discard after use can often times simply be committed, either as utility functions or test cases. Far from pulling up the ladder on devs who come later, having a REPL makes it more likely for them to have access to the same tools used to build and understand the system in the first place.


> But in the worst case scenario, you'll end up with extremely unmaintainable code.

Actually, isn't it far worse than that? Say you're stopped at a function call, you call it, and it does the wrong thing. You edit a variable that's passed as an argument to the function and call it again. Now it does the right thing. Great! You fixed the bug, right? Except that you don't actually know that your program can actually ever end up in that state naturally.

Where did that variable originally come from? Was it entered by the user? Was it read from a file? Was it from a table? If so, does the table contain the modified value? This seems like a recipe for problems to me. I'm sure it's fine in simple cases where you're just changing the value of a constant or something, but it seems really likely to lead to incorrect reasoning about the code while you're working with it in any but the simplest of cases. Am I blowing this out of proportion?


Well, no, you didn't fix the bug. You collected a datum about the bug: it caused a variable to have a value that it should not have had. Now you know something about it that you maybe didn't know before.

When you say "Great! You fixed the bug, right?" it sounds like a paraphrase of a passage in my essay that I had some misgivings about when I wrote it. But I thought, surely nobody's going to think I really mean the problem is definitely solved? Surely, people will realize its a bit of hyperbole.

Maybe not. Maybe I should have instead written it more soberly. Maybe I should have just said "Now you've collected a bit of data about the nature of the bug."


> The problem is that the reader of your code is not going on that journey with you. They are coming in naively without knowing all of that experimentation you did. The writer now has the opportunity to create code that "just happens" to work by a magic of coincidences and conveniences based on very subtle non-obvious properties of the APIs and systems they are working with. The reader is severely disadvantaged and will have a much harder time to reach parity with the state of knowledge that the writer had. For example, some property of a function or object may not be documented at all, but through the REPL the writer has ascertained it is true. How can the reader know this?

If these sorts of things aren't documented then not having used a REPL won't save you.

Someone ran a function with a println or in a debugger, figured out the answer, then removed the debug stuff... but did it by repeatedly compiling/running instead of in a REPL.

(Or, my personal favorite... someone didn't bother to run that particular code, but just made some assumptions, so has no idea those edge cases are even there...)


I really don't follow. If you're programming in a language without a REPL, don't you have the exact same problem? If the author doesn't document their knowledge, then it's lost for readers.

I don't really see how a REPL makes any difference there.


> the more you empower the original creator of the code, the greater imbalance you create to the later readers

REPL gives you more tools to shoot yourself in the foot, but you're not required to use those. Most programmers can write spaghetti code in many Turing-complete languages - not that they do that all the time.

In other words, REPL gives you powerful tools - which, yes, can be misused. They are still powerful, and can bring success when used well.


When a certain thing is done in repl, it usually converts into a function or two in the source code. The ad-hoc testing code is converted into unit testing.

That should help with maintenance no?

One big theoretical advantage I see in repl driven development is, I can be sure that _all_ the code has been executed at one time atleast. (In a normal edit-compile-link-test language cycle, I can not be sure of that)


Yep, just copy-paste a bit of REPL history for a docstring, then use `doctest` to make sure it holds. Pretty soon you'll have ~100% test coverage. Minus the error paths you didn't test in REPL.


Sure, but the point is that it requires more discipline, since the REPL can encourage you to be more lazy/sloppy.


Only if you aren't delivering something. No one (sane) is delivering a lisp image as "The Product" without also having a way to regenerate that image. If you write something in the REPL and never convert it to a proper function/class/struct/package in source, you're hosed when your system reboots and that image is lost.

This requires no more discipline than, say, actually committing files to a version control system to collaborate with others. Versus shouting from the hills, "It works on my machine!" and being confused when it turns out that you didn't commit "foo.c" and compiled it manually rather than updating your Makefile and committing both changes so others could use it.


> No one (sane) is delivering a lisp image as "The Product" without also having a way to regenerate that image.

Oh man, you are so wrong about that.

It's worth noting that this style of development also hamstrung quite a few Python web app projects in the late 90s because the Zope application server encouraged the use of this style of development (except through a browser rather than the command line) and beginners eventually got to a point that they were stuck and needed to make the leap to a completely different workflow of development based on files on the filesystem and in version control instead for their code, while content (ie. application state) for each deployment of their app remained in the embedded object database.

A frequent lament was "if you knew I would eventually have to switch to filesystem based development, why didn't you have me start out that way?"


This is how any major CMS works to this day, and anyone doing serious development does have the engineering workflows in place to replicate the database content.


You're missing that Zope application-development-through-the-browser could (and did) allow you to do just about anything with a Turing-complete template language, and if that was too unwieldy, Python script objects (which executed in a sandbox to prevent direct access to the filesystem, etc.).

Nothing forced you to a saner development model except for wanting to include 3rd-party libraries and being able to distribute and version your code.

Remember, this was when servers were definitely pets and scaling a web app meant scaling up to a bigger server, not scaling out to more servers. It wasn't immediately obvious (especially to brand new developers) that having all the code for even a simple CRUD web app, which you only ever expected to have a single deployment to a single server, live as pickles inside an object database that had a transactional history of edits, was inherently a bad idea.

A lot of interesting stuff was created that way, and integration with the facilities that were missing such as version control led directly to capabilities like versioning content in cvs and svn (since code was just another kind of content).

Eventually, Zope's so-called "Z-shaped learning curve" hindered adoption and other Python-based web-app platforms surpassed it in popularity.


Nope I am not missing that, because enterprise CMS still allow that kind of stuff.


Well, sure, but they aren't usually presented as the default path for development, but rather as an option for per-instance customization.

But if you understood my point about developing new functionality this way, why the non sequitur regarding content?

Y'know what? Nevermind. We're on the same page now.


I used the "sane" qualifier for a reason. Development like that is insane and moronic. It's unsustainable in the long term, and is not a counterargument to the idea of REPL-Driven Programming. REPL-Driven Programming does not preclude sanity and replicability and source files.


> For example, some property of a function or object may not be documented at all, but through the REPL the writer has ascertained it is true. How can the reader know this?

Well, the writer could document their code.

Or, the reader could discover behavior of undocumented code the same way the writer did; presumably the “reader” will also have a REPL available.


I think your concern is valid, but I think it's the general case with any powerful tool: it can be abused, or shoot you in the foot, and requires discipline to apply.

I don't have REPL-driven development experience, but I think we are all familiar with StackOverflow. This is a great, great resource, but it lead scores of programmers to the "style" of development, where they mindlessly copy solutions from SO, without understanding of how and why they work.

I guess one can extend the old maxim "there is not now, nor ever will be a programming language that makes it easier to write a good program than a bad one"...

(edited grammar)


So REPL may make people less likely to document the code?

I don't see the correlation.


I agree with you that the immediate start-up and feedback is a great benefit to the coder. This is why I dislike complex, Rube-Goldbergian REPL systems.

There is a use-case for a throw-away interaction with a REPL. For example, how does $builtinFuncX work, or how would $data best be imported into a structure?

A REPL can also be a good initial approach to a more ambitious problem. In this case, a REPL can be good for focus and discipline.

If the second case is going to answer your concern and be constructive, it's necessary to be able to build the code for sharing and cleanly export the code for re-use.

I've had success tackling challenges using REPLs for Python and Perl [1] in both ways. But no tooling is going to solve the problem of a sloppy teammate who claims success just because "it compiles" and "it works on my box". A person who knows how to build good tooling goes further.

[1] https://github.com/viviparous/preplish


100%, and that also applies to the original author.

Building a script from the REPL might mean running different parts of the code out of order (such as reloading a function definition). As you mentioned, it's easy to lose track of state, writing 'clever'/unmaintainable code, and forgetting the order and purpose of the code.


Code developed in a REPL would ideally be "pure", meaning it takes a value and returns a value without side-effects. Then others can experiment with it to their hearts content. Maybe it's ok to read from a database, but don't write. If you write to the database nobody else will dare touch your code.


That rule absolutely does not work for me.

For me, the whole point of the repl is that I want to build something incrementally, interactively, building it a little at a time by making small changes to it. I want it in memory, running and responding to my changes as I make them. Put this here; put that there. Change that around. Let me look at it; nope. Put that back where it was. Now add this.

If the repl is stateless then the whole purpose is defeated.


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.

Rinse. Repeat.


If there was a way to “playback” the REPL and store it for later, then it would be possible for the subsequent users (or even the same user) to see how the person using the REPL got to that point. It would not be a replacement for documentation, but it could be in addition to documentation or even a starting point.


More than Lisp, SQL is a REPL-only language. The complexity you can achieve there without some interaction is very small.

After some on the field experience, my conclusion is that this is not a large problem. Yes, you were not there on the initial design and does not have all of that acquired knowledge, but you can always run your own tests and formulate and verify your hypothesis. The REPL is there for debugging too, not only for the original design.


>SQL is a REPL-only language. The complexity you can achieve there without some interaction is very small.

ha ha.

unless it's giant sql with 5 sub queries and poorly formated for bonus points

good luck


Hum... 5 sub queries? Nope, I'm talking about bigger stuff.


Could this issue of expressing the journey be addressed by leaving code/markdown breadcrumbs in Juptyer notebook like cells?




Consider applying for YC's Fall 2026 batch! Applications are open till July 27.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: