My limited understanding of Clojure's HMR is that there aren't n distinct versions of a class/function. It's actually removing the old ones from memory and replacing them.

Also, it is worth noting that everything is immutable, not just data. Functions/protocols/records/etc are also immutable.

Functions/classes being immutable makes it much, much easier to reason about the dependencies of that function/class. E.g. check out this Python code:

import someotherpackage
import threading
import random

class MyLogger:
def __init__(self, name, path):
self.path = path
self.name = name
def log(self, message):
with open(self.path, 'w') as fd:
fd.write(message)

my_logger = MyLogger("init-logger", "init.log")
someotherpackage.logger = my_logger
someotherpackage.logger.error = print

print = someotherpackage.logger.error
my_logger.thingy = "thingy"

def change_logger():
def actually_do_change():
time.sleep(random.randint(1, 100000))
new_logger = MyLogger("main-logger", "main.log")
someotherpackage.logger = new_logger
someotherpackage.logger.error = my_logger.error
time.sleep(random.randint(1, 10000))
threading.Thread(target=actually_do_change).start()

time.sleep(random.randint(1, 10))
threading.Thread(target=change_logger).start()

Now imagine that this isn't our `main`, this is some library that's a transitive dependency of something we're importing.

Trying to hot-swap this would be awful. The big issues related to mutations are that a) there's no indication in someotherpackage that it's behavior depends on this package, and b) because mutations are allowed, the order that things are reloaded in matters, and c) some mutations are time-bounded.

If I change MyLogger and the VM/interpreter wants to hot-swap MyLogger, it has to recognize that it can't just re-load the instances of MyLogger with the same state. It has to re-load those instances, then re-mutate them, then re-mutate someotherpackage, and finally re-mutate the print function. It might also need to do the stuff that's delayed by that thread. Maybe. Depends on a couple of random integers that probably got GCed a while ago.

If you want to add an extreme layer of annoying, consider that this package could be a late import and only happens if a plugin is enabled, so the earlier code might rely on `print` actually being `print` or on someotherpackage.logger behavior that changes over time.

None of that applies in an immutable world. Nothing can mutate the someotherpackage package nor the `print` function, ergo nothing can depend on an earlier or different version of them. Dependencies are easy to track because the only way to introduce them is to import/directly reference them, or have them passed as parameters.

I don't know that it even needs any dependency tracking, though (provided they're using a pointer to a pointer, or maybe something smarter than that I can't think of). Immutability means that a) everything can be passed as pointers safely, b) those pointers can't be modified by the code, and c) there is never a reason to have more than one copy of a pointer to the bytecode for a function.

Each function gets a pointer to its bytecode, let's call that p1. Every reference to that function is a pointer to p1, which we'll call p2.

When you want to hot-swap code the language pauses the VM/interpreter, recompiles any function with a diff (easy with an AST), puts the new functions in memory somewhere new, changes p1 to point to the new bytecode, then marks the old bytecode for GC.

If you tried something that simple in Python for the code I wrote above, it would explode. It's an infinitely harder problem.

> Using Java I can have n distinct versions of a class each implementing the same interface in the same process as long it is the interface reference that is passed around.

It's been a while since I worked with those app containers, but from my recollection it shares virtually nothing with hot reloading. E.g. is state preserved between those? If I'm caching stuff in RAM, and I change that class out, does it keep the cache? HMR does, as I recall.

My understanding is that app containers basically just run N versions of your app/class and allow you to choose which one you route execution to.

App containers are more similar to blue-green deployments with a load balancer. Each instance is totally separate, and the load balancer lets you choose which one you route to. App containers just do that process inside the JVM.

That's not bad, but it's also not as good as being able to repeatedly tweak a function without ever clearing caches or re-connect to downstreams or etc.