Sometimes mutable state is good, or at least practical. For large or complex projects (more than a school assignment), it's not a good default. Mutability introduces a brittleness which amplifies as code scales-up.
What's so fragile about mutable state? You can reference a value before it's changed or after -- one of these cases is almost certainly wrong in a given situation. Not only is this easy to get wrong (writing), but it's easy to misinterpret (reading), and changes to code might fall afoul of this explicit (yet subtle) sequencing.
Someone gave me an example of how it's more intuitive to code like this:
balance = balance + deposit // or balance = balance - withdrawal
And cited that "the real world is mutable, so it's natural to represent things this way."
"The real world is mutable" -- is it really? One could just as well consider the world-state as new variables with time-subscripts. This would convey the variance through time, yet also formalize the apparent lack of classic time-travel. "What's past is past." (And: immutable)
let newbalance = oldbalance + deposit
Now you can refer to both, and if you move some statement using
newbalancebefore this one, you get an error. With just
balance, you could move something intending to operate on the updated balance to happen before the update -- changing results.
It's a simple thing, and here it might seem more verbose, but as you build code into compositional expressions, you can connect parts together without so many explicit, intermediate "state containers" -- instead you're handing-off results to the next step: dataflow.
Objects, as they are commonly used, are morsels of mutability wrapped in a package which makes them seem safe. Object-oriented programming had grand promises of modularity and safety, which I think have been hindered by mutability being the default.
The big problem with global variables was (and is) mutability -- not their scope. A global constant isn't a worry, is it? With objects, it's like we stash "global variables" into a more restricted scope, but the mutability is still a problem. We'll hide things behind abstractions so we aren't updating a variable directly using language primitives (eg +=), and we'll try to limit the scope of code which can touch certain objects... but in practice we encounter the same race-conditions and multiple arms of code stomping on data that global variables suffer -- the problem is only somewhat contained.
To re-iterate: the black-boxing lends a sense of safety to stashing mutable state everywhere with complex interfaces as gatekeeper. So we practice this -- stashing little state enums, counters, flags... all these mutable bits in objects that we twiddle from code everywhere (but using the objects' nice functions to do it). How far did we really get from those global variables? It's the same style of code, just organized a bit better (hopefully). Sometimes mutable state is good, but a variable should have a good reason to be mutable, rather than this being the default.