[HN Gopher] The one about Lisp interactivity
       ___________________________________________________________________
        
       The one about Lisp interactivity
        
       Author : lycopodiopsida
       Score  : 63 points
       Date   : 2022-11-11 10:54 UTC (12 hours ago)
        
 (HTM) web link (blog.fogus.me)
 (TXT) w3m dump (blog.fogus.me)
        
       | Barrin92 wrote:
       | 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/
        
       | mixedmath wrote:
       | 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...
        
         | dimitar wrote:
         | The post by David Vujic actually mentioned the significant
         | difference - REPL-driven in the way Clojure programmers
         | understand it is not simply interpreting snippets of code in a
         | separate process, but interacting with a running program,
         | including all it's state and modifying it on the fly if needed.
         | 
         | Clojure programs listen on a extra port that you can connect
         | your editor to and modify the program as it running.
         | 
         | Things like tests and notebooks may run the same code, but
         | don't have the exact state and environment as your running
         | program, be it running locally a staging env or even
         | production.
         | 
         | This is not unique to Clojure, a Common Lisp program was
         | famously running on a space probe and REPLed into decades ago,
         | and Smalltalk code is stored in VM images, but few mainstream
         | languages nowadays allow such interaction with a running
         | program
        
         | iib wrote:
         | 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.
        
       | mark_l_watson wrote:
       | Nice writeup! Interesting comparison of OCaml (fast tooling but
       | not connected to running program and data).
       | 
       | Lisp REPLs are something I use every day, but I also enjoy REPL
       | development that is connected to look I've program and data with
       | Python with Emacs, Ruby (when I used to use it), Haskel,and
       | Julia.
       | 
       | The REPL is a way of life.
        
       | zen21 wrote:
       | 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.
        
         | synthc wrote:
         | 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.
        
         | dimitar wrote:
         | 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.
        
           | synthc wrote:
           | 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.
        
             | tonyarkles wrote:
             | 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.
        
         | ballpark wrote:
         | 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
        
       | gaze wrote:
       | 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. To me, the understanding that
       | some set of things may be replaced right under you adds a great
       | deal of additional engineering complexity. I've gotten various
       | answers about how one deals with this. One being "yes add the
       | machinery" or "just yolo it, yes it's a race condition but it's
       | rarely an issue," neither of which I find particularly
       | satisfying.
       | 
       | Concerning state, I've occasionally found myself developing a
       | long-lived lisp image, and then I need to restart the VM for one
       | reason or another, and then found that nothing works. The state
       | of the in-memory image had gotten totally out of sync with the
       | codebase. Perhaps this is a manner of discipline in lisp, but I
       | greatly appreciate the replacement of discipline (be it memory
       | management or the aforementioned situation) with machinery of the
       | language itself.
        
         | mark_l_watson wrote:
         | On LispWorks REPL, I use the Threads and Processes Viewer.
        
         | Jtsummers wrote:
         | 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.
        
           | sillysaurusx wrote:
           | > 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" <enter>, 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. But of course
           | then the tradeoff is that you have to be careful that you
           | only save to disk when your code isn't a syntax error, ha.)
        
             | tonyarkles wrote:
             | > 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
        
         | Joker_vD wrote:
         | > how you manage state
         | 
         | It's already done for you by the runtime: it holds the state in
         | the process memory. Now, with edit-compile-run-print loop if
         | you want your state to persist between runs, you gotta
         | implement some scheme of data persistence.
         | 
         | > 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.
         | 
         | In Erlang, you create a public named ETS table (basically, a
         | thread-safe dictionary) and put "global settings" in there
         | instead (and read them from there, too). I'd imagine LISP has
         | something similar.
         | 
         | And mind you, I personally prefer the ECRPL instead of REPL,
         | even when working in Python.
        
         | jgalt212 wrote:
         | > The state of the in-memory image had gotten totally out of
         | sync with the codebase.
         | 
         | I have not used it, but I heard that Clojure Clerk is supposed
         | to help with this.
         | 
         | https://github.com/nextjournal/clerk
        
         | sillysaurusx wrote:
         | 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.
        
       | effnorwood wrote:
        
       ___________________________________________________________________
       (page generated 2022-11-11 23:01 UTC)