First-class support for interactive use in the language?

Interactive tools like Read-Evaluate-Print Loop (REPL) and notebooks are useful tools for language exploration, running experiments with complex setup unsuitable for unit tests, hot reloading in web/app/game development, data analysis, etc.

Many typed and compiled languages have good or first-class support for interactive use. Examples include Cling for C++, Swift, OCaml and Haskell REPL, Dart hot reload, and recently Mojo. They show it can be done with IR interpretation, JIT, or AOT compilation.

Currently, Rust's only persistent-state interactive tool is the community-driven Evcxr REPL and Jupyter kernel. It is quirky in my experience though, probably because it invokes Rustc for each input, creates a shared object, and links it. I believe including such tools directly into the language could greatly benefit their functionality.

As Rust 2024 edition is coming, let's revisit this issue from 2015: implement a proper REPL · Issue #655 · rust-lang/rfcs · GitHub.

Related: Why does Rust lack a useable REPL? - The Rust Programming Language Forum, State of rust repls? - #6 by stevedonovan - The Rust Programming Language Forum, Is there are any analogue to Scala's Worksheets or REPL for RUST? - The Rust Programming Language Forum, Question regarding refactoring Rust code and REPL support - help - The Rust Programming Language Forum, Repl / shell rant - #4 by ehsanmok - The Rust Programming Language Forum, REPL, RAII, Static Types - #10 by anon80458984 - The Rust Programming Language Forum, Is there something like REPL? - help - The Rust Programming Language Forum, Next steps for reducing overall compilation time - #11 by Soni

2 Likes

When I last looked into this space, I felt cargo script would be a big first step. It doesn't give us full interactivity but it covers enough of the use cases that having it built-in would be a big help. The RFC for cargo script is now in FCP and is mostly implemented in Cargo already.

While not focusing on a REPL at this time, I have been collecting notes on them at What about a REPL? · epage/cargo-script-mvs · Discussion #102 · GitHub

I've wondered if a notebook system might be easier to align with Rust than a REPL with minimal loss. I've been wanting to explore a markdown notebook system that supports multiple kernels as an extension of my trycmd crate.

Overall, I think more experimentation is needed outside of the Rust toolchain to better understand what we should bake into the Rust toolchain.

9 Likes

Cargo script would make Rust much nicer for scripting. Thanks for the great work! Though, it is very far away from something like a REPL, where a runtime is necessary to persist state between input blocks, sometimes even MBs of generated code and GBs of data loaded.

One tricky problem we seem to have is the chicken-egg problem. On the one hand, as you said, more experimentation is needed to know what is good; on the other hand, not many people are demanding stories of Rust's interactive use, presumably because those people already lost hope and turned away in Rust's 9 years of not having a REPL…

Even when working in python I found notebooks to be a terrible developer experience.

  • You can rerun bits out of order, which mean you have no idea if the whole thing works (or what part depends on what other parts), this seems like the anti-thesis to Rust.
  • Code organisation becomes much more slapdash than with "proper" modules.
  • Debugging was quirky or out right broken.

The first two are inherent to the format, the last point could presumably be solved. In my experience notebooks are the worst of both worlds (REPL and traditional programs).

That said I don't know that Rust benefits from either notebooks or a REPL, there simply isn't much of a need for that sort of experimentation in Rust thanks to the type system. In Python a REPL is invaluable to figure out what the types actually are.

3 Likes

(post deleted by author)

While I agree with your thoughts on notebooks specifically, I think a Rust REPL would be great for experimentation.

2 Likes

I absolutely do have complex setups for testing. And I automate those. At work we even have physical hardware hooked up to test harnesses as well, with runs scheduled directly from CI. Saves a ton of time on development as well as the mandatory safety qualifications (I work on on human safety critical things).

I don't write games, but I do write applications, not just libraries. Data analysis is outside my expertise as well.

But I think you are mischaracterising my point of view here. When I wrote python I also did application development, not data analysis. And a REPL was needed there even for that. Even for library work it was needed in Python. However for doing application development in Rust it really isn't something which I have missed at all. And I attribute that to the type system.

Am I against a REPL? No, I'm just saying it doesn't seem like a high priority. I even installed evxcr half a year ago, played with it for 5 minutes and then never used it again. Rust playground and Godbolt fulfills this niche of testing small code snippets well in Rust I believe. But maybe doing data analytics is a completely different beast where a REPL is more useful.

2 Likes

Great! Now, you are providing insightful experience and making your point more nuanced after my mean bait (sorry for that).

Yes, I would say most logic that have somewhat easily-specifiable input, reasonable execution time, and no need for crazy rapid changes probably benefits not much from interactive uses (this is hard to coin). However, my point is that, additionally, if these are all you need, you would not feel that interactive uses are useful (in Rust)… and then what happens is, enough people say this, causing interactive use support to be deprioritized (recall the issue from 2015).

Well, sometimes the ability to interactively run something is not just for experimentation; it is a core deciding factor for usability.

People write game scripting engines in Lua. Flutter uses Dart JIT for hot reloading. Polars mainly suppose people would be using their Python bindings because Rust data analysis never blew up.

These can be changed, though.

2 Likes

Sure, one thing to keep in mind is that someone who actually find it useful is most likely the one who is going to champion this and also implement this (rust is open source, people work on pieces they themselves are interested in, or if this is their day job, that their employers are interested in).

If you wish to do so, here are some things people are likely to ask about, so it is better to come prepared in your argumemts:

Why can't this be done in a crate? / What is wrong with evcxr/why can't that be improved?

Can this be solved by adding some small feature(s) to rust itself that such external crates can use to do the job better? If not, why not?

Then there is the arguments I made above (type system means it isn't that necessary). Here you should not answer the way you did to me, people are generally ignorant of the fields they are not in. I see many people forgetting about the needs of embedded no-std async when they bash on the current approach of async in Rust. Just as likely people will be ignorant of the needs in your field.

Instead consider explaining what the workflow is, and showing the pain points of the current state (e.g. evcxr or no REPL at all).

And be prepared that it will likely take a lot of effort.

2 Likes

They're absolutely great for what they are. Butt they share a weak point, and that is that they both require an internet connection. This is fine most of the time, but this can make then unusable e.g. when traveling.

In addition, if a true Rust repl were available as a native binary, it'd reduce the feedback loop from:

  • remember URL for the playground
  • navigate to the playground
  • do the thing

To:

  • go to the (pretty much perms-open) rust repl in the terminal
  • do the thing

It may not seem like much of a difference, but for me the use case of a rust repl is quick experiments to verify/debunk some hypothesis regarding some Rust semantics or syntax. There, it is desirable to have the question-answer process be as short as possible, ideally mere seconds, assuming the code example allows for that. This then dovetails nicely with the short feedback cycle of a repl, as measured along 2 dimensions:

  1. Time
  2. The number of steps I need to take (or the amount of effort I need to expend, which correlates with that) to get an answer to my question.

A good repl implementation minimizes the values along both dimensions, accounting for the fact that Rust is not always the most terse language, which of course influences the feedback loop as well¹, and in fact might be one of the defining factors along with compilation speed of realistic pieces of code one would evaluate in a repl.

¹ The feedback loop cannot be shorter than the time it takes to input your query, after all.

1 Like

maybe a good solution without all the downsides of a REPL would be to just have a local playground -- a program you can run that pops up an editor and has a run/build/asm/etc. button.

1 Like

The main question I would have with any REPL for Rust would be "how does anything actually work?"

Like, what actually happens when I run "trait Foo {}" the second time - does that invalidate/remove all the existing impls? What about modules, can I "cd" into a module to access it's internal items?

There's so many differences like that, that I just can't figure out how it would be anything other than basically a new language, where the things you learn are pretty hard to figure out how to map back to "real" Rust.

You can hack some of the value of notebooks with inline unit tests, maybe something more along those lines would be more helpful? A way to let an editor compile and run a snippet against an existing compile of a crate for fast turn around?

4 Likes

You could take inspiration from the REPL for Erlang here. The language doesn't normally allow arbitrary code at the top level (just functions, records (structs), etc). I believe Haskell has a similar situation (though I have not used Haskell nearly as much).

In these languages you can execute expressions directly at the top level of the REPL as well as overwrite previous definitions. At least for Erlang I seem to remember that not all types of code were possible to write directly in the REPL (though it has been over a decade since I last touched Erlang). Maybe you couldn't define modules or records in the REPL, something like that.

Any RFC on this would need to look at prior art across a wide variety of languages to see how they handle these sort of issues. In particular languages that (unlike Python, Perl, etc) are "compiler first", where the language wasn't designed with a REPL in mind from the start.

1 Like

@epage's notes has some great stuff.

Personally, I would like to see Rust adopt the approach like Clerk or Livebook, where it analyzes the program flow and reruns only necessary parts on source code change, as opposed to Haskell, Julia, etc. whose REPL works slightly differently than the rest of the language.

That is, like cargo watch, but hot reloading.

1 Like

It should be noted that those languages were designed with a heavily dynamic environment in mind, while Rust is much more focused on the static part. I would not expect Rust to get their same capabilities, or at least I think it would be too big for an initial goal.

2 Likes

Exactly. Languages need to be designed from the ground up to support interactive use. Rust is not such a language right now, therefore any attempt to shoehorn it into an interactive environment would quickly reach some limit.

Lots of efforts need to go into the language itself first. This requires the language to change (which Rust can because we have editions), and the team taking it into the roadmap (which only happens with enough interest and feasibility).

Some of the features that make python work well in that environment are the opposite of why rust is great for other use cases. (I'm going to use python as the example here because I don't know lua or Dart.)

For example, python is a byte code compiled (with an experimental JIT in the works) and highly dynamic. It has runtime reflection. All of these are features that slows the code down, or bloats the compiled binaries.

What would happen in Rust if you reload a module and a struct now has a different layout? It works in python because everything is a dict (or at least has enough metadata to resolve things dynamically at runtime if you use slots). In Rust that metadata is missing in the compiled program. The generated machine code is accessing offsets directly. And for many uses (embedded for example) doing otherwise would add unacceptable bloat.

I'm not sure it is possible to make a language to please everyone. Rust is the successor to C and C++. It is spectacularly successful at this. People are not trying to do interactive data science in C++ as far as I have heard. Perhaps rust doesn't need to be everything to everyone. Maybe Julia with some heavy pieces written in Rust would be a better fit for that niche.

What you are saying here is Rust would need a completely different solution to support interactive use. Hence, my mention of Clerk and Livebook above.

Idea: Instead of baking knowledge of the types, etc. into the running program, only have them exist in a runtime (yes, surely needed for interactive use). The runtime tracks the control flow and definitions, and decide when to recompute some of them when code changes.

For you example, currently, in Rust, if you change data structure layouts before reloads, you either get undefined behaviors, or the tool just gives up on all old state to prevent crashes, e.g., Evcxr telling you something like "YourStruct is redefined, so was lost", or dioxus-cli recompiling the whole program.

1 Like

Interesting. For my part, I'd greatly prefer the Haskell-style approach that adapts the language slightly to being used in a REPL, rather than using something like a notebook. I'd like to just type a line at a time and have that work. If the language doesn't have enough information yet to run something (e.g. because it needs a later line before it can infer the type), it can either prompt for the type (with suggestions), or in some cases allow the type to remain ambiguous if it hasn't affected execution yet.

An example of the latter case: if you write let x; and then start writing a loop, the REPL can say "well, I know x is a variable, I don't know the type yet, but I can reserve the name x and wait to find out the type until later."

4 Likes