Just because something can change doesn't mean it can't be modelled with immutable data.
In OOish style, lets say you have something like the following:
a = some_obj.calculationA(some, arguments);
b = some_obj.calculationB(more, args);
return combine(a, b)
Obviously calculationA
and calculationB
depend on some_obj
, and you're even manually threading some_obj
through as inputs to both calculations. You're just not used to seeing that that's what you're doing because you think in terms of invoking a method on an object.
Translating halfway to Haskell in the most obvious way possible gives you something like:
let a = calculationA some_obj some arguments
b = calculationB some_obj more args
in combine a b
It's really not that much trouble to manually pass some_obj
as an extra parameter to all the functions, since that's what you're doing in OO style anyway.
The big thing that's missing is that in OO style calculationA
and calculationB
might change some_obj
, which might be used after this context returns as well. That's pretty obvious to address in functional style too:
let (a, next_obj) = calculationA some_obj some arguments
(b, last_obj) = calculationB next_obj more args
in (combine a b, last_obj)
The way I'm used to thinking of things, this is "really" what's going on in the OOP version anyway, from a theoretical point of view. Every mutable object accessible to a given piece of imperative code is "really" an extra input and an extra output, passed secretly and implicitly. If you think the functional style makes your programs too complicated because there are dozens of extra inputs and outputs all over the place, ask yourself if the program is really any less complicated when all that data flow is still there but obscured?
But this is where higher abstractions (such as monads, but they're not the only one) come to the rescue. It's best not to think of monads as somehow magically giving you mutable state. Instead, think of them as encapsulating patterns, so you don't have to manually write code like the above. When you use the State
monad to get "stateful programming", all this threading of states through the inputs and outputs of functions is still going on, but it's done in a strictly regimented way, and functions where this is going on are labelled by the monadic type, so you know it's happening.