[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)