Could you talk a little bit about what happens to processes which are running different versions of the code? Do you make your code backward compatible so that old processes continue to work with the new process? Or do you just let the old processes crash?
If you use OTP (gen_server, etc), your code is a set of callbacks that return promptly, so it never stays "old" until the next upgrade. Your callbacks produce and use a "state" object (opaque to OTP) that OTP passes around[1], and OTP calls a code_change method whenever your module changes, so you can change the format of this object between versions.
The gen_server itself passes its module name to sys:handle_system_msg[2] when it receives a system message like a code-change notification, which ends up being the Module:Function type of call that always refers to the new version of the module, so it always ends up running the latest version when you upgrade gen_server.
If you set things up right, the old process is upgraded to use the new code upon processing the next message (and has an opportunity to upgrade its state from the state the old code was using to the state the new code was using, if required); like, the idea is that you are trying to upgrade the running code. (In fact, leaving old processes lying around is actually problematic as you can only have two versions of any module loaded, the obsolete one and the current one; if you upgrade again all processes running the old version crash.)
You can upgrade state in many cases if the changes are trivial, though depending on the ephemerality of a process you might let it finish or crash and restart instead of doing this.
So, in the worst case, for achieving basic code reloading in other erlang like systems(ex.- cloud haskell), we could let every process in the cluster crash and restart with the new version?
It's rarely "only crash" as that eventually bubbles up to being equivalent to a reboot. It's simply that you can have subsystems that do that in effective isolation if the effects of a reboot are minimal.
On the code state upgrade side there are usually a few different issues that can arise and I'd be very curious to hear if there are ways Haskell would handle some of these.
The first one is type changes. I might have a record that has a new field added. Now it's not necessarily pretty to upgrade on call with a pattern match or using a code upgrade protocol but it's easily expressed dynamically.
Another is in the interface, like adding new arguments or changing from a synchronous call to an asynchronous one. These are a bit easier to handle via indirection though they show that you'll need to plan your entry/exit points for upgrades carefully (again, OTP has things like gen_server which make this much easier).
If Haskell can manage to get past they type boundary issue then it's really a matter of supporting at least 2 simultaneous versions of code so each process can be scheduled and upgraded in natural course. Handling more than 2 could be of use depending on how aggressively you want to purge, for example, a local rather than fully qualified call can be caught in a closure and passed around as in some value to be called later. These long lived references will need to be handled carefully or you might get some delayed surprises.