Live data from Hacker News

The one about Lisp interactivity

blog.fogus.me

11–20 of 51 posts

Re: The one about Lisp interactivity

#11

In the opening paragraphs, this post indicates that this other post of David Vujuc [1] falls victim to common misconceptions about what a REPL is. I'm not sure what misconceptions this post is referring to; perhaps I also have these misconceptions. But I also don't think this post clarifies that. Could anyone here make it more explicit? [1]: https://davidvujic.blogspot.com/2022/08/joyful-python-with-r...

Lisp programmers usually use the term "REPL" closer to its original form. read, eval, print, loop are primitives in Lisps. They consider what other languages call a "REPL" to be interpreters or mere command-line interactive interfaces.

Programmers in other languages usually suffice to calling an interactive interface a REPL.

Re: The one about Lisp interactivity

#12
post #4

The thing I still don't understand about REPL driven development is how you manage state, and how you manage threads. If I have a task running and it depends on a collection of global variables, those global variables now need some machinery around them so that they can be edited from the REPL thread. If you replace a function, there now needs to be decisions made about when you now begin usage of the new function. T…

The "discipline" is to put the source code into a source file, the same thing you do with every other language. Don't do this and expect positive results:

  SOME-PACKAGE> (defun a-critical-function (...) ...)
The discipline to instead do this:

  ;; some-package.lisp or whatever
  (defun a-critical-function (...) ...)
Is table stakes for other languages, and Lispers are no worse programmers than programmers in other languages so why would they be incapable of this basic discipline? Only fools would write that in the REPL, never commit it to a source file, and be surprised when they couldn't reproduce the system state later on.

Same with loading data. It comes from a database, file, or other source. Preserve the method of loading the data in source like all other code.

Re: The one about Lisp interactivity

#13
Smalltalk only got a mention in the footnotes but I think it deserves a bigger entry when talking about ways to interact with programs. If a REPL is talking to and conversing with a program then environments like Pharo take it a step further by letting you interactively and graphically look under the hood as well. It's an amazing way to interact with software once you get used to it and I think well worth checking out if only for fun. https://pharo.org/

Re: The one about Lisp interactivity

#14
post #8

This seems to leave out the existence of debuggers in IDEs. These days I tend to run most new code in the debugger, and step through it so I can see what’s going on. This works great in say, Rust, Python, or Swift. I haven’t used Java for a while, but I don’t see why it wouldn’t work well there too. I feel very connected to the code.

I used the Cider debugger with Clojure with some success, but as I got better at REPL-driven development I ultimately stopped firing up the debugger

Re: The one about Lisp interactivity

#15
post #8

This seems to leave out the existence of debuggers in IDEs. These days I tend to run most new code in the debugger, and step through it so I can see what’s going on. This works great in say, Rust, Python, or Swift. I haven’t used Java for a while, but I don’t see why it wouldn’t work well there too. I feel very connected to the code.

True, for Java and Kotlin I use the debugger in IntelliJ like a REPL all the time. You can even evaluate arbitrary expressions, connect to running applications even applications running on another machine.

It's not as powerful as a proper lisp repl as you cannot redefine functions or classes, but it is still very useful.

Re: The one about Lisp interactivity

#16
post #10
post #8

This seems to leave out the existence of debuggers in IDEs. These days I tend to run most new code in the debugger, and step through it so I can see what’s going on. This works great in say, Rust, Python, or Swift. I haven’t used Java for a while, but I don’t see why it wouldn’t work well there too. I feel very connected to the code.

The author does mention debuggers. But you don't normally run a debugger on production or a testing environment and with a REPL you can. A debugger situates a program differently than it is normally used, a REPL connects to your code/state/environment in any stage of development.

I would also not normally connect to a REPL in a production environment, but it can come in handy.

One can certainly attach a debugger to a running process in production, I've done it several times to debug gnarly issues.

Re: The one about Lisp interactivity

#17
post #4

The thing I still don't understand about REPL driven development is how you manage state, and how you manage threads. If I have a task running and it depends on a collection of global variables, those global variables now need some machinery around them so that they can be edited from the REPL thread. If you replace a function, there now needs to be decisions made about when you now begin usage of the new function. T…

You’re right. I think the other commenters aren’t being straightforward with you. It is a race condition to load a file one function at a time, because any other thread can preempt you.

Arc has a particularly elegant solution to this. Any code you want to happen atomically, you wrap in (atomic …)

So

  (atomic x y z)
Will do x, y, then z. During this time, no other threads are allowed to run.

Therefore your load-file function might look like (pseudo code because phone):

  (def load (filename)
    (atomic
      (each form (read-file filename)
        (eval form))))
Now any time you call (load “code.arc”), it’s guaranteed that none of the other threads will run “mid-update”. If your file contains definitions that overwrite all functions in your program, then your entire program is guaranteed to update atomically.

Under the hood, (atomic …) is implemented with a recursive lock (cf. Python’s RLock). That way, if you call a function that calls atomic, which calls another function that calls atomic, you won’t deadlock — it’s the same thread, and the same thread can always acquire the lock recursively.

And that’s it. The lock is literally a single instance of a recursive mutex, stored globally, created at program startup.

Astute readers will notice one pitfall: suppose there are 10 threads running, and then you load file, which replaces all of the functions those threads were running. What happens?

Each thread is paused in the middle of some existing function. That function will continue to exist until nothing refers to it. Since those threads refer to those functions (because we’re paused at some spot in the function), the currently-executing functions wont vanish until all the threads wake up and return.

… which is particularly problematic if your thread is a while true: “do this forever” loop! There’s no way to update it anymore. You’d have to kill the thread and restart.

Which is why the solution is “don’t do that, do this.” Get rid of the while loop, and call yourself recursively. Now whenever the new function loads, calling that function by name means you’ll jump into the new function, abandoning the old one.

In languages without tail recursion (lookin at you Python, bastard), you can still achieve this by making sure your thread runner is a while-true loop that just calls some other function, and nothing else.

Re: The one about Lisp interactivity

#18
post #4

The thing I still don't understand about REPL driven development is how you manage state, and how you manage threads. If I have a task running and it depends on a collection of global variables, those global variables now need some machinery around them so that they can be edited from the REPL thread. If you replace a function, there now needs to be decisions made about when you now begin usage of the new function. T…

The "discipline" is to put the source code into a source file, the same thing you do with every other language. Don't do this and expect positive results: SOME-PACKAGE> (defun a-critical-function (...) ...) The discipline to instead do this: ;; some-package.lisp or whatever (defun a-critical-function (...) ...) Is table stakes for other languages, and Lispers are no worse programmers than programmers in other languag…

> Only fools would write that in the REPL, never commit it to a source file, and be surprised when they couldn't reproduce the system state later on.

This happened a few times while I was developing https://laarc.io. It’s so addictive to just paste new functions into a repl and see the changes instantly that it’s easy to go a few hours without committing and realize your changes rely on a now-deleted function. :)

Wouldn’t trade that workflow for the world though. C++ compile/run separation makes it horribly obvious how much productivity you lose. It feels like walking into a pit of molasses. But even Python isn’t much better — I’m constantly tabbing to the repl, typing ctrl-R “reload” , then pressing the up arrow twice to evaluate the previous function example I was working on. I wish there was an auto reload feature that would just reload everything every time I evaluate a repl expression.

(I’ve been writing Bel lisp in Python, and I put in some code to do just that for bel.interact()’s repl. It’s so much nicer not having to reload manually all the time.)

Re: The one about Lisp interactivity

#19

Earlier quoted context omitted.

The "discipline" is to put the source code into a source file, the same thing you do with every other language. Don't do this and expect positive results: SOME-PACKAGE> (defun a-critical-function (...) ...) The discipline to instead do this: ;; some-package.lisp or whatever (defun a-critical-function (...) ...) Is table stakes for other languages, and Lispers are no worse programmers than programmers in other languag…

> Only fools would write that in the REPL, never commit it to a source file, and be surprised when they couldn't reproduce the system state later on. This happened a few times while I was developing https://laarc.io . It’s so addictive to just paste new functions into a repl and see the changes instantly that it’s easy to go a few hours without committing and realize your changes rely on a now-deleted function. :) Wo…

> It’s so addictive to just paste new functions into a repl and see the changes instantly that it’s easy to go a few hours without committing and realize your changes rely on a now-deleted function. :)

I think Slime is a pretty happy middle ground. Instead of just pasting, the function gets written in a source file and then C-c C-c to evaluate it in the repl. I did once or twice run into similar issues though from depending on older or newer versions of functions that changed but weren’t completely reflected until a clean restart. But that’s been pretty rare and definitely wasted way less time than “make clean all” before committing C++ code :D

Re: The one about Lisp interactivity

#20
post #16
post #10

Earlier quoted context omitted.

The author does mention debuggers. But you don't normally run a debugger on production or a testing environment and with a REPL you can. A debugger situates a program differently than it is normally used, a REPL connects to your code/state/environment in any stage of development.

I would also not normally connect to a REPL in a production environment, but it can come in handy. One can certainly attach a debugger to a running process in production, I've done it several times to debug gnarly issues.

I’ve done it in Elixir a handful of times to try to understand a bug that I couldn’t reproduce in my dev environment. And a few times to run some queries for a client and dump them out to CSV.
Post reply on HN