This shows exactly the problem but perhaps not in the context you're thinking of.
One of the biggest strengths in processes in Erlang is that they serialise access, this serialisation though isn't simply that they serialise or are written in a way of "run this once". It's more like processes are (or can be used) basically state machines. This pattern is pervasive and you can find it in the OTP library, the most common construct (although there's proper finite state machine OTP abstractions) is the gen_server. The gen_server, is a generic server interface that uses message passing. It can be described as the simplest of agents that has a baked in generic interface.
Where your sample falls short is that, when you code the interface for a gen_server I'm (or can, usually will) codify any needed relationship between the state a process is in and what it can do with the "messages" it receives according to the state it is in. This allows me to completely forgo of any concern about how someone will use it without having to explicitly define handling of cases I didn't intend.
If I have (pseudo code but completely codifiable):
- when msg and msg == "get_status" and state == "receiving" -> reply, function_that_calculates_response(state), new_state = make_new_state(state)
- when msg and state == "not_available" -> reply unavailable, new_state = state, continue_to: "recheck_assumptions"
- when "recheck_assumptions" and state -> noreply, new_state = check_assumptions(state)
- when msg, state -> reply bad_request, new_state = state
Now I don't have to worry about any nook, escape hatch or other such in the language, that someone will rewrite the state of this process from another thread, or function call or whatever. Nowhere will a process be able to change the state of this process from outside the process meaning I can clearly define the states it can assume. I also don't need to write overly defensive code, or find out that in some given situation something changed the state outside of what I explicitly coded.
In the previous example this means that if two processes (running in different schedulers, nodes, or wtv) both send a msg, the mental model of the receiving gen_server will be a consistent mailbox from which it picks msgs to process one at a time. If one of the msgs would force the gen_server to change its state, and another asks for the status, it won't happen that both of them can have their conditions evaluated at the same time, but the states differing.
So if msg_1 changes the state, msg_2 will be interpreted on this new state. It can't happen that msg_1 is being processed, changes the state in some intermediate step, while msg_2 is interpreted in the same exact conditions as msg_1 (unless msg_1 leaves the state as it was). This is not true for some mutable code, you can have two functions from different threads or whatever, call a function on X concurrently, and the evaluations for these calls being done based on the same state, while then fun1 and fun2 operates on the state. Perhaps fun2 assumed that it was allowed because of being in a given state, but while doing its thing that assumed that, the state had been changed by fun1 that was running at the same time.
It also means, that this process can do whatever it wants, including spawning new processes and whatever, to do their thing in different contexts but its state can only be mutated by itself. This then forces those things (or me when writing code) to explicitly define their interface and if I want to rely on the result of those things changing the state, I need to explicitly message pass it back, for which I will have the previously defined interfaces.
This then when coupled with supervision, links or monitors can make for extremely resilient processes even when they encode complex flows between multiple parts in the presence of possible unknowns.
Can you still bork it? Yes, but the area is much smaller and I guess after a while of working with it it's a very simple model that once understood is always the same.
Are there areas where this is not the right away to do it and mutable is right? Probably yes, people point out performance issues but I think there's also some overstating about it, in most cases it seems it can be solved, but I can believe there are possible issues there so not a silver bullet.
Sorry for the long winded post.