Why your REPL experience sucks

(srasu.srht.site)

86 points | by tosh 507 days ago

11 comments

  • jwr 507 days ago
    A related question is why would you want to reload the entire file/namespace every time you make a change?

    Coming from a Common Lisp background, I am used to evaluating single forms/functions. I would sometimes re-evaluate the entire namespace, but that would be relatively rare.

    I switched to Clojure many years ago and followed the same workflow. Then ClojureScript and tools like figwheel came along and I was somewhat surprised to find that people admire the "auto-reload" functionality so much: save a ClojureScript file and all of it gets re-evaluated in the browser interpreter. I found this to be inefficient, somewhat annoying, and a big step back from evaluating exactly what you wanted to.

    My guess is that lots of those people never worked with a REPL where you eval single forms from the editor, so this auto-reload on each save seemed much better. But it isn't! And it creates its own problems, like the one the OP solves in the article.

    • electroly 506 days ago
      This feels a little too strong to me. DrRacket has as its fundamental UI flow a combination of whole-reloaded-file and a REPL, and obviously the creators of Racket are intimately familiar with Lisp REPLs.

      One problem being addressed is the discrepancy between REPL-based development and non-image-based languages: you have to get your code back out into a source file somehow, without forgetting anything. In image-based languages like Smalltalk and APL, you just save your environment after modifying it interactively; there are no separate source files and the problem does not exist. But in file-based languages, you're stuck using the clipboard to copy code back and forth between the REPL and the source file. It can sometimes be easiest to write the code in the source file, then reevaluate it in the REPL environment. If that process is automatic instead of highlighting specific forms and pressing a button, it can sometimes be a slight productivity boost and less error-prone because you won't forget to reevaluate some change you made in the source.

      • Jtsummers 506 days ago
        > In image-based languages like Smalltalk and APL, you just save your environment after modifying it interactively; there are no separate source files and the problem does not exist.

        My experience with APL was not based on using the image (longterm), but with Smalltalk, there is a source file paired with the image (you can see this with both Squeak and Pharo). And Iceberg has been around for years allowing you to select specific portions to export to or import from git repos as source files. It was preceded by other version control systems that let you export to and import from source files.

        • mumblemumble 506 days ago
          It's historically been pretty janky, though, and an acknowledged problem within the Smalltalk community. I think Pharo perfected their solution only within the past 10 years.
        • electroly 506 days ago
          Definitely. Dyalog APL has support for source files, too, but I didn't want to overcomplicate things. The way that it saves environments (i.e. to a binary file or to a source file) is tangential to the point: that it has the ability to save the environment at all. REPLs go best with languages that can save their live environments, so that you don't have to worry about getting your work back out of the REPL environment. If your language can't save its environment, you need some other way to improve the REPL-to-source workflow.
      • greggman3 506 days ago
        A system I used you'd edit the source and press some key-combination that injected the current function only from the editor to the running app
    • TylerE 506 days ago
      No friction is better than friction.

      Having to manually reload parts of a file is friction, and introduces possible heisenbugs... e.g. A,B, C were changed, but you forgot to reload A.

      There were good reasons for doing it that way when we had 10000x less computing power, but these days? Nah..

      • pletnes 506 days ago
        Having spent tons of time in jupyter notebooks, I can tell you one thing - loading data and training ML models is not free. Keeping a running system/repl/notebook has immense value.
    • suskeyhose 506 days ago
      So I am no stranger to CL, but the problem solved in the article isn't actually one that's solved by evaluating less than the whole file at once, and in fact I didn't tell anyone to re-evaluate the whole file at all in this article.

      The critical idea here is it applies to long-running functions, like a loop, a server, or other things. These, when handed function objects, will not reflect new behavior if you re-evaluate a single function.

      The article solves for this by explaining to intermediate programmers that there is a difference between using function objects and using vars, something that Common Lisp programmers learn too by using #' to retrieve function objects and passing symbols and using symbol-function to look up the associated function as needed.

      • dmux 506 days ago
        >The critical idea here is it applies to long-running functions, like a loop, a server, or other things. These, when handed function objects, will not reflect new behavior if you re-evaluate a single function.

        Do you mind going into more details about what you mean here? In my experience, recursive loops will pick up a new function definition but non-recursive loops will only pick up the definition of a function upon entry.

        • suskeyhose 506 days ago
          The idea is such a long lasting loop would take the function object as an argument and call it repeatedly. Recursive or not (tho in clojure's case definitely not, no tail call optimization), doesn't matter as the lookup happened at the first function call.
      • lispm 506 days ago
        CL: in a primitive way this can be done with generic functions. Use a generic function object. Redefined methods will be picked up without needing a new function object.

        When using symbols, often it is not needed to look up symbol functions. Symbols can be called as functions. In code Lisp often assumes that functions are late bound (unless inlined), so a function call to a global function looks up the function from the symbol at runtime.

        • suskeyhose 506 days ago
          This is kinda my point tho, CL's symbols are the same as Clojure's vars for this purpose. And the comparison between generic functions and multimethods is obvious and correct here as well.

          Late binding is irrelevant because all of this is about functions as arguments, not in funcall position.

    • simongray 506 days ago
      Hot reloading is useful when you're making something related to a React-based UI, which you likely are close to 99% of the time in ClojureScript. I seriously doubt it has anything to do with a lack of REPL experience. I use a traditional editor-connected REPL 100% of the time in Clojure and 0% of the time in ClojureScript. I think most of us doing full-stack Clojure work like that.
      • jwr 506 days ago
        How is it useful? I've been developing a React app for the last 8 years or so, and I'd much rather eval single functions rather than reload everything. I feel much better in clj and cljc files, where I am not forced to do that.
      • kelseyfrog 506 days ago
        I used hot reloading extensively when developing a roguelike in Clojure. It was incredibly helpful to have a tight feedback loop.
    • onetom 506 days ago
      I have a shortcut, which reloads all modified files, then re-evaluates the previously evaluated expression, within the context of the same namespace it was evaluated in previously.

      This way I can be in any file, modify any `def` of `defn` and see its effect on the expression I'm working on.

      After modified something, I might jump to its unit test or jump to some definition of some adjacent vars, where i see a point which I want to probe (with a function like this: `(defn ? [x] (pprint x) x)`).

      Then I have 2 modified files, and my cursor is in a 3rd file and the expression I'm iterating on is in a 4th; doesn't matter, I can just keep hitting the same shortcut (Cmd-Ctrl-Enter) to see the effects of my changes on the "expression-under-test".

      It's an extremely convenient workflow, which is also easy to teach to ppl!

      Downside is that you can't have namespaces, which have side-effects WHEN they are loaded.

      To deal with that problem, I'm using `redelay`, `rmap`, `meta-merge` & `defonce`, eg:

          (ns service
           (:require
             [meta-merge.core :refer [meta-merge]]
             [gini.rmap :as rmap :refer [rmap ref] :rename {ref $}]
             [redelay.core :as redelay])
      
          (def kit
            (merge {:cfg {:db/uri "..."}}
                   (rmap {:db (connect (:db/uri ($ :cfg)))})))
      
          (defonce service-ref
            (redelay/state
              :start (rmap/valuate! 
                       (meta-merge service/kit 
                                   {:cfg  {:param 'override}
                                    :some 'special-component}))))
      
          (defn service [] (deref service-ref))
      
          (comment
            (.close service-ref)
            (-> (service) (some-operation x y z))
            )
      
      More info on this topic is here: https://functionalbytes.nl/clojure/rmap/2020/07/04/rmap-2-up...
    • capableweb 506 days ago
      > I switched to Clojure many years ago and followed the same workflow. Then ClojureScript and tools like figwheel came along and I was somewhat surprised to find that people admire the "auto-reload" functionality so much: save a ClojureScript file and all of it gets re-evaluated in the browser interpreter. I found this to be inefficient, somewhat annoying, and a big step back from evaluating exactly what you wanted to.

      It seems to be the default in some tooling in the ecosystem, but you can turn that off and use the "regular" Clojure way in ClojureScript environment too. That's what I usually tend to do.

      > And it creates its own problems, like the one the OP solves in the article.

      What OP solves in the article doesn't seem to be about reloading an entire namespace, AFAIK. It's certainly not common in the Clojure ecosystem to reload your entire namespace instead of just the function you're working on.

    • aidenn0 506 days ago
      FWIW, I just use C-c C-k with CL because it's easier. The standards lawyer in me also wants to point out that implementations are free to inline function calls that are in the same file as the function definition, but I'm not aware of any implementation that takes advantage of this, so that's rather academic.
    • TacticalCoder 506 days ago
      > A related question is why would you want to reload the entire file/namespace every time you make a change?

      Wait... What change does it make to evaluate a single function or to evaluate that single function plus re-evaluate all the other function in the same Clojure source file? It's not slow. It doesn't break anything?

      Regarding figwheel, which I find very convenient (it reloads not just the modifications in .cljs files but also in HTML and CSS files AFAICT), where is the problem and would you reproduce what figwheel does without figwheel?

      Most of my Clojure and ClojureScript source file have zero state, zero variable. And from what I can tell many Clojure projects are like that. So what change does it make if I define (or redefine) a function from a REPL or from the source file and have figwheel re-evaluate the whole source file?

      Heck... I got "confused" at some point: if I modify a .clj file I need to eval what I modified (or the whole file), to have my REPL "synched" with the file. While with figwheel I don't need to eval but save the file for figwheel to update the front-end's JavaScript in realtime.

      So what I did is this: I modified my shortcuts that does "eval buffer" to do "save + eval buffer" and modified my shortcut that does "save" to also do "save + eval buffer".

      That way everything works in the same manner and I don't need to remember which one does what.

      If I modify a .cljc source file (common to both Clojure and ClojureScript) and either eval the file or save the file, both REPLs see the new definitions and figwheel also hot reloads. I really like that.

      And, yet, I still have my two REPLs at all times and I can directly eval stuff at the REPLs if I want to.

      > My guess is that lots of those people never worked with a REPL where you eval single forms from the editor, so this auto-reload on each save seemed much better.

      OK but for ClojureScript running on the front-end, how would I then go, without figwheel's hot reload, to make a modification and have the (transpiled) JavaScript be updated immediately?

      > And it creates its own problems, like the one the OP solves in the article.

      But the problem of ring routes not being updated have nothing to do with auto-reload? I may be wrong but wouldn't the problem be exactly the same if I were to re-eval the router var from the REPL (I'll try it and see later on)?

      Anyway I'd say that, when worse comes to worse and you screwed your application's state / components lifestyle, a full restart one every blue moon ain't a biggie.

  • PaulHoule 507 days ago
    Workspace-based programming environments have always struggled. When I use Jupyter I rerun the whole book pretty frequently. If the book takes 30 seconds to run it is a big annoyance but saves time relative to the randomly irreproducible problems I see other people have. If the book takes 3 hours to run it is a different story.
    • all2 506 days ago
      The idea of "book" and the linear visual in a "book" limit the interaction of the user.

      What if, in addition to the "book", you had a visual representation of the environment? Further, what if you had a function application visualization?

      Say you start with

          with open(your_csv_here) as my_csv:
              ... some actions
      
      In the function application viz you'd see something like

          your_csv_here
            |
          open as
            |
          my_csv
      
      In the environment visualization you'd see two variables

          your_csv_here
          my_csv
      
      Ideally, you'd be able to grab either of those in the "book"'s linear representation of code blocks using auto-complete.

      ---

      When I've played with data sets in Pandas DFs, for example, having some awareness of my environment and the available states of data -- and how that data has been mutated -- would be extremely helpful.

      ---

      I've taken the view that a Jupyter notebook or similar environment represents a filter for one or more streams of data. Ideally, you need to be able to quickly visualize the execution environment and previous actions/filters applied to your data streams. I'll admit, this does nothing for working with logic (except that you'd be able to inspect inputs and outputs pretty quickly).

    • nerdponx 506 days ago
      Fixing this situation is supposed to be the promise of "reactive" notebooks like Julia's Pluto.
      • PaulHoule 506 days ago
        That's one answer to the problem, but not the only answer or necessarily a complete answer... That is, people forget that the really special thing about spreadsheets is not the grid organization but the hidden graph organization of computations.

        I would point to this as a product that was ahead of its time and still seems without peer

        https://en.wikipedia.org/wiki/TK_Solver

        Another challenge Jupyter has is the conflict between "a way to present a report" and "a way to write a script that generates a report" and everything in between.

        • eigenspace 506 days ago
          > That's one answer to the problem, but not the only answer or necessarily a complete answer... That is, people forget that the really special thing about spreadsheets is not the grid organization but the hidden graph organization of computations.

          Uh did you mean this the other way around or something? Reactive notebooks like Pluto only address the hidden graph organization of computations, they don't address grid organization at all.

  • aidenn0 506 days ago
    Leaving wrong explanation for posterity and to keep thread understandable. See everything after [edit] for corrected explantion

    This is an important distinction between (funcall #'foo ...) and (funcall 'foo ...) in Common Lisp as well. #'foo evaluates to the function named by the symbol foo, while 'foo evaluates to the symbol foo itself. So if you redefine the function named by the symbol foo any already compiled forms like (funcall #'foo) will still use the old version but (funcall 'foo) will use the new version.

    [edit]

    The above explanation is wrong; whenever #'foo is evaluated it will resolve to the current function slot for the symbol foo. Since the form (funcall #'foo) evaluates #'foo each time, it will naturally be affected by changes to the function-slot for foo. If you bind a variable to #'foo (like e.g. a route in a routing library as TFA does for clojure) then the evaluation happens when you bind the variable. See my reply to throwaway1x31 for a full example showing the difference.

    • throwaway1x31 506 days ago
      In sbcl those two work the same.
      • aidenn0 506 days ago
        Derp, of course it does because (funcall #'foo) evaluates "#'foo" every time it runs. You need to bind (or assign) the value for it to actually show the difference:

            (defun bar () 3)
            
            (defvar *the-fn* #'bar)
            
            (defvar *the-sym* 'bar)
            
            (defun foo ()
              (funcall *the-fn*))
            
            (defun baz ()
              (funcall *the-sym*))
        
        Then at the REPL:

            CL-USER> (values (baz) (foo))
            3
            3
            CL-USER> (defun bar () 2)
            WARNING: redefining COMMON-LISP-USER::BAR in DEFUN
            BAR
            CL-USER> (values (baz) (foo))
            2
            3
      • juki 506 days ago
        They don't. What aidenn0 said applies to all CL implementations, although with a small correction that it's not "any already compiled forms", but rather any variables/objects that already hold the value of `#'foo` that will keep pointing to the old definition. Compiled code calling `(funcall #'foo ...)` will get the new definition normally (unless `foo` is inlined).
  • markeibes 507 days ago
    Whenver I use a REPL I'm annoyed I didn't write it as a test, so I get proper import completion and generally tooling support. Finally I usually want to reuse the code or have a regression.
    • capableweb 507 days ago
      I think you're thinking about a different type of "REPL" than what the author is referring to. What you're thinking about is more of a "Programming Shell", a separate window/tab/pane where you enter code and when you're happy, you copy-paste that into your source file.

      What the author is talking about is a REPL running in the background while connected to your editor. So most of the time, you're entering code directly into your file, while evaluating snippets in the REPL but you never leave the editor itself.

      With that approach, nothing is stopping you from using the REPL to write the tests themselves. In fact, that's what I tend to get the most value from, writing unit tests together with a background REPL, interactively building up the test case until I'm happy, then committing the unit test that has been written in the process.

  • TacticalCoder 507 days ago
    I really only ever encountered this issue when working with ring routes, as in TFA. Love the macro at the end, may start to use it because why not.

    Now I want to say this to Clojure beginners: launching Emacs with a very complicated, non optimized, init (3400 lines of custom elisp code I wrote over the years) takes 1.4s on my system. Now launching the app, with two REPLs (one for Clojure on the back-end, one for ClojureScript on the front-end I'm testing) and about 30 dependencies (including figwheel, which is kinda heavy) takes...

    16 seconds.

    So, from complete scratch, launching Emacs, opening a Clojure source code file, (which btw also triggers the launch of the Clojure LSP server) and then launching the webapp server, two REPLs and loading the main webapp page in a browser takes less than 18 seconds. Well, ok, it's not from complete scratch as source files that haven't been modified do not need to be recompiled, I'll grant that.

    It's a 2019 AMD 3700X with 32 GB of RAM and a NVMe M.2 PCIe 3.0 x4 lanes SSD. Not a bad machine but not last gen either (I'm probably swapping it for a 7700X one of these days).

    I'm pretty sure the startup time is going to go significantly down when I'll switch to the 7700X.

    So even though it's great to not have to restart the app/REPLs it should not be a problem to restart the whole thing.

    If your REPLs take two minutes to start, I'd argue there's an issue and it's time to fix that first. Move from lein to deps.edn (which launches one JVM less, which really helps as Clojure startup times on the JVM are slow), buy a faster dev machine. Buy a PCIe SSD. Use chrome/chromium instead of Firefox for testing your webapp (in my experience chrome is simply way faster for anything JavaScript and, oh boy, does ClojureScript generate lots of JS). Etc.

    As a sidenote I'd say this specific "ring route not updated" is a variation of the age old issue of cache invalidation.

    So basically: I love working at the REPL and keep my REPL(s) opened for a very long time, hot reloading everything (including ClojureScript on the client-side) without needing to restart everything but I don't lose sleep over the fact that I do sometimes relaunch everything for it takes not even 20 seconds.

  • AceJohnny2 506 days ago
    Non-Clojure user here (but with just enough Lisp experience to make a fool of themselves)...

    > When Clojure evaluates the symbol router [(an argument)] here, it goes through almost the exact same process as it did for the symbol +, but without checking if it’s a special form. It looks for the var in the current namespace that maps to the symbol router, dereferences it, _and saves the function object it retrieves as the first argument before evaluating the second argument_, and then calling the function that run-jetty evaluated to.

    Why does it save the resolved function object, which is an argument at that scope? That sounds counter-productive to the reloadable functionality.

    Is it a performance optimization?

    If anything, it demonstrates that reloadable code is inherently at odds with performance.

    • jaccarmac 506 days ago
      No expert on Clojure semantics, but I'm pretty sure what's being discussed is common to almost all runtime environments. When you pass an argument to a function, the function doesn't keep track of the name of whatever you pass, it keeps track of the thing you pass. (A few languages bind late enough that you are essentially passing names during function invocation, but AFAIK not many.) The difference is that when you redefine a function in Clojure, you're not updating the memory location of the old function but updating the function that a given var (one of Clojure's synchronization primitives) points to. The var quote, then, is roughly the equivalent of passing a pointer, while the naked syntax is roughly like passing a dereferenced pointer. I find the "save" language in TFA a bit confusing.
      • AceJohnny2 506 days ago
        Thanks, that makes it a bit clearer.
  • nikanj 507 days ago
    We keep on making fun of php, but it had this problem solved in the 90s. Hit F5 in the browser, get the latest version of your software. No waiting 30s for a rebuild, no stale versions in caches, no headaches.
    • iLemming 506 days ago
      > Hit F5 in the browser, get the latest version of your software.

      What about the state though? If I'm building a sophisticated app, let's say an e-commerce thing, imagine having to develop a shopping cart experience, where a user has to make multiple choices before getting to the shopping cart view. Every time you hit F5, you'd have to click a bunch of buttons, fill out fields, etc. Or you're gonna have to write a script that takes you there and restores the state, which is still time-consuming and annoying to have to do. With Clojurescript, I can change things in the code, eval it and the change would be reflected in the app immediately, without having to reload the entire app.

      > it had this problem solved in the 90s

      We weren't building many complex web apps back in the 90s, so the problems associated with hot-reloading weren't so apparent back then.

      • cess11 506 days ago
        You could put some state in the session to keep it persistent across page reloads and get a result similar to continuations or however you're keeping that state on the JVM.
    • freedomben 506 days ago
      Modern PHP can be like the REPL experience though depending on situation. Half the time the new code loads, the other half the time it serves from cache (opcode cache?). I'm not a PHP person so I'm probably doing something dumb, but I've found that inserting a `systemctl restart php-fpm.service nginx.server` into my test workflow avoids that problem.
    • cess11 506 days ago
      These days we also have psysh.org which is a pretty sweet interactive PHP shell.

      Doesn't have the sophisticated halting and rewinding and redefining that Common Lisp and other image based REPL:s do, but it allows for very rapid development in a similar way.

    • tonetheman 506 days ago
      PHP was and still is great. Make a change and hit refresh. Simple and easy.
      • mm007emko 506 days ago
        I upvoted your comment but I only partially agree. PHP is great in some ways and totally atrocious in other ways. It put bread on my table in the 2000s but I am not looking back. The language is horribly designed. Writing AJAX calls before Google Chrome was even released (let alone its V8 Engine which made JavaScript fast) was no fun either. Any non-trivial web app we developed was slow (everything had to start from scratch with each request) and flooded with a couple of layers of caching which often didn't get invalidated properly.

        Now I am a purely backend&desktop developer, I leave web frontends to people who are willing to live in the JavaScript world where if no big framework is released every 5 minutes you have an impression that the Earth stopped spinning. I learnt the basics of React.js the other year and it's a far cry from what we had 20 years ago. ClojureScript+React even with its difficulties must be an ivory tower of modern web frontend development. I want to remember the good parts of PHP, frameworks Nette and Symfony with nostalgia. But I never want to re-live it again.

        I mean Request/Response websites with a bit of AJAX still work today, I even develop one as a side project since I am part-time involved in academia (neural networks & nature-inspired algorithms simulator, in Common Lisp with cl-who, Parenscript and a couple of custom components based on HTML5 Canvas). But modern development? When you need to keep up with the latest trends? No way it can work.

      • cutler 506 days ago
        The Clojure repl allows you to eval changes within a live application. Try that in PHP and I don't think you'll get very far.
        • cess11 506 days ago
          What do you mean by "live application"? Is a PHP application under nginx "live" or not?

          If it is "live", changing a file will immediately be reflected in the application on the next "eval", i.e. require or execution by the interpreter.

          • klibertp 506 days ago
            Nope. That's not what the GP means. I'm not sure how it works currently with PHP, but what was meant was a long-running process that can change its behavior/be updated by evaluating new code without ever being restarted. Think of it like this: you have a page that never stops rendering, ie. you send some HTML but instead of finishing processing, you go into an infinite loop. "Live" system, in that situation, would still be updatable - you could change the body of the loop and make it produce another bit of HTML, without reloading the page.

            Again, I'm sure modern PHP was already optimized far from the initial "execute all the code on each request" model, but unless there's a long-running process that can be updated in memory without restart (and I don't mean doing `exec` on itself, that's cheating!) - it's not "live" by this definition.

            It's not that clear-cut a distinction, either. For example Elixir and Phoenix are definitely live, in that there's a process that responds to requests on one hand, and can be updated without restarting it on the other - but page reload is still required in many cases, even if triggered via websocket.

            This idea of liveness predates both PHP and the Web by 2 decades at least and comes from Lisp and Smalltalk. It's been incorporated by Erlang and later Elixir, and by Clojure. Other than that, some embedded scripting languages support this kind of development, for example Awesome WM is scripted in Lua, and you can evaluate arbitrary Lua code while it's running. Emacs is another system that is meant to be used this way.

            Good to read: https://gbracha.blogspot.com/2020/01/the-build-is-always-bro...

    • agumonkey 506 days ago
      php exchanged restart time for debugging time
  • cpursley 506 days ago
    I've only used a few REPLs so far but Ruby and Elixir are great (and I assume inspired by Lisp). I cut my teeth developing with REPLs and actually struggle a lot with trying to use language environments without them. Like, how can I just experiment and run a little one-of bit of code and data before starting the actual implementation? Or call and play around with the function I just wrote?
    • onetom 506 days ago
      Have a look at Fred0verflow's videos, for example:

      * https://www.youtube.com/watch?v=St0_jsPnGAw - analyze some SO HTTP API responses * https://www.youtube.com/watch?v=TaazvSJvBaw - transducers from the ground up

      I think they are great, crystal-clear examples of REPL-based development.

      • cpursley 506 days ago
        Oh, I think you misunderstood. I'm literally in the Elixir repl all day every day and do know how I'd don't know how I'd manage without one.
        • onetom 506 days ago
          If the Elixir REPL is like a Ruby, Python or Node.js "REPL", then we are not talking about about the same kind of REPL in this context.

          I suspect, that the Elixir REPL is more like the Erlang REPL, which might be the same kind as LISP REPLs, but I have no direct experience, I just saw Erlang REPL usage from John Hughes videos.

  • runevault 506 days ago
    I used to be a big REPL head, especially in my time with Clojure. But in recent years since most of my programming was c# I got away from thinking in REPLs. However these days I'm doing personal work in F# so I have access to the REPL again, I just keep not thinking to take advantage of it. I guess this is my reminder to use ALL the tools a language/environment gives me.
    • cutler 506 days ago
      The kind of repl the author refers to is strictly Lisp-based. Your F# repl wouldn't be any different from a Python or Ruby repl and falls well short of the Lisp/Clojure repl experience which is based on the code-as-data feature of Lisps.
      • runevault 506 days ago
        I'd have to check and see, but I know at least some of the features overlap (for example f# lets you send a single function/etc to the repl and update the existing definition. However to the specific point he brought up, I'm not sure if anything like the var capture issue comes into play.
        • cutler 505 days ago
          The difference between the Lisp REPL and other, Algol-based programming shells (Node, Python, Ruby etc.) is in the R which stands for Read. The Reader is unique to Lisp since Lisp syntax approximates to an Abstract Syntax whereas other language shells have to parse syntax as raw source code during this first stage. The Eval stage is also different with Lisps.
  • uptownfunk 506 days ago
    R Studio repl experience one of a kind, have never found anything that replicates it perfectly.
    • mm007emko 506 days ago
      I always preferred Matlab to R Studio, TBH.