Are there any interactive / live tools for Scala that allow redefining methods without restarting a running program?
For a bit more context, I am thinking of long running programs with lots of state that you don’t want to lose by restarting, like a game engine or data analysis pipeline. You want to make a change to behaviour while preserving current state.
Some language ecosystems have developed interactive / live reload / hot reload techniques that make this possible, so I am curious if this exists for Scala.
(I recently asked this on Discord as well, but I thought posting here may solicit additional responses.)
I’m not aware of any tooling to do this easily – others might come up with options, but I haven’t noticed any. It’s a just-plain-challenging problem in the JVM and similar environments.
I’m pretty sure it can be done, by using class loaders and encapsulating the hot-swappable code, but there are inevitably limits to how much you can do there without compromising type safety and the like, since it means that you’re making runtime changes outside the scope of the compiler. I wouldn’t recommend doing it casually: it wants really careful upfront design of what you’re trying to accomplish and where the boundaries are.
I’ve done stuff like this in C#/.NET, a million years ago (when I was building what amounted to a container-ish middleware engine), and I’m reasonably confident it can be done on the JVM. But it’s not a common use case, so I’m not sure there are any libraries to make it straightforward.
I should also note: the same goals can often be accomplished other ways. For example, this sort of warm-update isn’t rare in the Akka world, where the application and its state is distributed across multiple nodes. You don’t swap out methods per se, but you can bring up a node on a newer version of the application, and then migrate actors (which contain the state) over to the new nodes. The system as a whole keeps running, even while individual nodes are gradually updating under the hood. That’s still not simple to do reliably (you need to be careful about protocol evolution), but it’s not a bad approach when always-up is a priority.
I haven’t seen any issues related to it, but it might have been because people didn’t actually use it If you have any issues let us know! It should just be a case of starting the program within metals and using the reload button (thunderbolt)
I thought the play framework supported hot reloading, and there’s sbt-revolver though the hot reloading part doesn’t seem to be actively supported anymore IIUC. But AFAIK that functionality is usually only meant to be used during development for quick turn-around, not for rolling out new versions in production.
Aha, very cool! I’ve done a quick test just now, and it does indeed appear to work as intended. I can change a method while debugging, click hot code replace (bolt), and the new behaviour is used on the next call to the method. State seems to be preserved as well.
One small issue I noticed: When I click the hot code replace (bolt) button, I get a warning from Metals that says “No classes were reloaded”, but in fact they were since the new behaviour is applied correctly… I filed an issue for this. I’d like to start contributing to Scala tooling, so I’ll attempt to fix it.
Yeah, as far as I can tell from searching around, low-level tooling (agents, class loaders, etc.) would need to be created to do anything more complex than method redefinition, as I believe that’s the only change directly supported by most JVMs.
Some customised JVMs (JetBrains runtime, GraalVM Espresso) appear to support additional code changes at runtime, though I haven’t tested those myself (and there may be Scala-specific hurdles to overcome).
I’ve also noticed a few mentions of JRebel (in the Java ecosystem) which claims to also support advanced code changes, but it’s a commercial offering.
I’m happy to create any tooling (open source, of course!) that might be needed to enable this kind of workflow (in case I find a need to go beyond the method redefinition that Metals + JVM already supports). As you say, it’s critical to keep the compiler involved to ensure type safety and such.
If other people are also interested in interactive / hot code replace features, please do let me know.
That seems like a very interesting challenge: Typechecking the changes
I see it as ensuring there exists a “bubble” around the changes on the surface of which the types (and binary representation) remain the same before and after the change
A few examples:
val x: Int = 4
// can become
val x: Int = 5
// x in scope
val y = 5
foo(x, y)
def foo(x: Int, y: Int)
// can become
val y = "Hi"
foo(x, y)
def foo(x: Int, y: String)
class A(x: Int)
val z = A(1)
// cannot become
class A(val x: Int)
val z = A(1)