I feel this is an overblown problem. The number of times this has been the cause of a bug for me in the last 5 years is zero. Avoiding shared state mutation isn't especially hard in imperative languages: when it happens it's because you're dealing with another constraint - i.e. memory size or wanting to avoid copying due to speed constraints.
What is a constant problem is when it turns out you have the wrong value going into the wrong thing: and that's either a logic bug where your inputs are all fine, or a function which is returning the wrong thing. Debugging this problem isn't helped by avoiding mutation really: you're still going to be stepping through every line and checking function results (and a lot of functional styles are really annoying like this - z(y(x(a))) x(a) y z if you're allowed a point free style might save some space, but usually a debugger has no idea how to show you the intermediate results).
Accidental mutation doesn’t happen very often. But when the type system doesn’t distinguish between mutable and immutable values, it becomes hard to reason locally about the code. You never know for sure if that value that looks like it shouldn’t be mutated isn’t maybe mutated after all under some circumstance five function calls away. Or, conversely, if you add a mutation somewhere, it can be difficult to exclude the possibility that some code elsewhere relies on that value not changing. Instead of being able to prove the correctness of some local piece of code, or some locally applied change, it remains merely a good guess, unless you maybe do full-program analysis. That for me is the actual benefit of immutable types — you can sensibly reason about them without having to defensively copy them all the time.
I’m in favor of some statefulness, by the way, not a fan of pure FP languages.
> But when the type system doesn’t distinguish between mutable and immutable values, it becomes hard to reason locally about the code. You never know for sure if that value that looks like it shouldn’t be mutated isn’t maybe mutated after all under some circumstance five function calls away.
I don't think that mutability has to be an inherent property of a value: it's really a description of how a value can be accessed. Personally, I'm partial to the model used by Rust, where you can either have shared immutable access to a value, or exclusive mutable access to a value, and you can temporarily loan out your access to another function with a strict deadline (a "lifetime"). Together, this means that you can call a function granting it mutable access to your value, and be confident that the called function is the only thing in the world that could have modified the value.
I'm surprised by how this "controlled mutability" solution never seems to come up in discussions of imperative vs. functional programming. Perhaps it has to do with how mind-boggling the borrow checker can appear coming from most other languages.
> Avoiding shared state mutation isn't especially hard in imperative languages
You're right, it isn't, and I wasn't trying to claim it was a particularly big problem. What it is is another little you don't even have to think about which I found to be a pretty delightful feeling. Cloning isn't a consideration in mutable languages and that's a nice a little win. Though as a sibling comment mentions, if you're working with a bunch of inexperienced folk, these things are a lot more likely to slip through.
As for the debugging bit, I made the sin of comment before I read the article and realized it was using JS. No matter what anyone wants to tell you, JS is very good as a functional language. It can be ok, but it's not great. "Most functional styles" is otherwise a very unfair statement as most functional languages come with pipes or infix application operators that make debugging really easy as you can throw an `inspect` into a pipeline.
Getting a bit off the rails just as I was excited about this, but Elixir recently introduced a `dbg` function which, if used at the end of a pipeline, outputs every line of it which is crazy nice (unsure if this was borrowed from another language or not):
In mutable languages we can't be sure that `perform_some_action_with_object` doesn't mutate `user` so we have to check. This isn't even a consideration in immutable languages. Again, not the biggest problems in the world, but it really helps when you want to quickly scan through a codebase you are unfamiliar with. You could also say "don't write code like that" and I tend to agree, but I have seen a lot of code like that in OO codebases in my time.
I made sure to double check my spelling this time :D
You may have worked on better codebases that I have. I have seen plenty of "let's write that stuff to a global or thread-local variable because" which makes code harder to understand, test setups harder and may even lead to subtle bugs.