The one thing that is unequivocally bad practice here is claiming that something is a pure function when it isn't.
If mutable variables are used in a way that is truly and completely self-contained, the function is externally pure and everyone is happy. Haskell in fact supports this explicitly, with the type system even ensuring that mutable references can't be used outside the function that creates them.
That said, I think talking about "side effects" is not the best way to look at it (and is why I said "pure" above). Anything that creates a dependency between the function and external state makes things harder to reason about, and that includes things like knowing the current time or using concealed mutable state in a non-thread-safe way.
Can we really use immutability in OOP without losing all key OOP features?
Don't see why not. Been doing it for years before Java 8 got all functional anyway. Ever heard of Strings? Nice and immutable since the start.
- I start needing persistent (in the functional sense) data structures like lists, maps etc.
Been needing those all along as well. Invalidating my iterators because you mutated the collection while I was reading it is just rude.
- It is extremely inconvenient to work with cross references (e.g. tree node referencing its children while children referencing their parents) which makes me not use cross references at all, which again makes my data structures and code more functional.
Circular references are a special kind of hell. Immutability wont save you from it.
- Inheritance stops to make any sense and I start to use composition instead.
Well here I'm with you but don't see what it has to do with immutability. The reason I like composition isn't because I love the dynamic strategy pattern, it's because it lets me change my level of abstraction.
- The whole basic ideas of OOP like encapsulation start to fall apart and my objects start to look like functions.
I shudder to think what your idea of "OOP like encapsulation" is. If it involves getters and setters then just please stop calling that encapsulation because it's not. It never was. It's manual Aspect Oriented Programming. A chance to validate and a place to put a breakpoint is nice but it's not encapsulation. Encapsulation is preserving my right to not know or care what's going on inside.
Your objects are supposed to look like functions. They're bags of functions. They're bags of functions that move together and redefine themselves together.
Functional programming is en vogue at the moment and people are shedding some misconceptions about OOP. Don't let that confuse you into believing this is the end of OOP. Functional and OOP can live together quite nicely.
Really that it. Dykstra told us goto
was harmful so we got formal about it and created structured programming. Just like that, these two paradigms are about finding ways to get things done while avoiding the pitfalls that come from doing these troublesome things casually.
Let me show you something:
fn(x)
That is a function. It's actually a continuum of functions:
f1(x)
f2(x)
...
fn(x)
Guess how we express that in OOP languages?
n.f(x)
That little n
there chooses what implementation of f
is used AND it decides what some of the constants used in that function are (which frankly means the same thing). For example:
f1(x) = x + 1
f2(x) = x + 2
That is the same thing closures provide. Where closures refer to their enclosing scope, object methods refer to their instance state. Objects can do closures one better. A closure is a single function returned from another function. A constructor returns a reference to a whole bag of functions:
g1(x) = x2 + 1
g2(x) = x2 + 2
Yep you guessed it:
n.g(x)
f and g are functions that change together and move around together. So we stick them in the same bag. This is what an object really is. Holding n
constant (immutable) just means it's easier to predict what these will do when you call them.
Now that's just the structure. The way I think about OOP is a bunch of little things that talk to other little things. Hopefully to a small select group of little things. When I code I imagine myself AS the object. I look at things from the point of view of the object. And I try to be lazy so I don't over work the object. I take in simple messages, work on them a little, and send out simple messages to only my best friends. When I'm done with that object I hop into another one and look at things from it's perspective.
Class Responsibility Cards were the first to teach me to think that way. Man I was confused about them back then but damn if they aren't still relevant today.
Let's have a ChessBoard as an immutable collection of immutable chess pieces (extending abstract class Piece). From the OOP point of view, a piece is responsible for generating valid moves from its position on the board. But to generate the moves the piece needs a reference to its board while board needs to have reference to its pieces.
Arg! Again with the needless circular references.
How about: A ChessBoardDataStructure
turns x y cords into piece references. Those pieces have a method that takes x, y and a particular ChessBoardDataStructure
and turns it into a collection of brand spanking new ChessBoardDataStructure
s. Then shoves that into something that can pick the best move. Now ChessBoardDataStructure
can be immutable and so can the pieces. In this way you only ever have one white pawn in memory. There are just several references to it in just the right x y locations. Object oriented, functional, and immutable.
Wait, didn't we talk about chess already?
Best Answer
I think the importance is best demonstrated by comparing to an OO approach
eg, say we have an object
In the OO paradigm the method is attached to the data, and it makes sense for that data to be mutated by the method.
In the Functional Paradigm we define a result in terms of the function. a purchased order IS the result of the purchase function applied to an order. This implies a few things which we need to be sure of
Would you expect order.Status == "Purchased"?
It also implies that our functions are idempotent. ie. running them twice should produce the same result each time.
If order was changed by the purchase function, purchasedOrder2 would fail.
By defining things as results of functions it allows us to use those results without actually calculating them. Which in programming terms is deferred execution.
This can be handy in of itself, but once we are unsure about when a function will actually happen AND we are fine about that, we can leverage parallel processing much more than we can in an OO paradigm.
We know that running a function wont affect the results of another function; so we can leave the computer to execute them in any order it chooses, using as many threads as it likes.
If a function mutates its input we have to be much more careful about such things.