I've supported an application where 'everything is a map' before. It's a terrible idea. PLEASE don't do it!
When you specify the arguments that are passed to the function, that makes it very easy to know what values the function needs. It avoids passing extraneous data to the function that just distracts th programmer - every value passed implies that it's needed, and that makes the programmer supporting your code have to figure out why the data is needed.
On the other hand, if you pass everything as a map, the programmer supporting your app will have to fully understand the called function in every way to know what values the map needs to contain. Even worse, it's very tempting to re-use the map passed to the current function in order to pass data to the next functions. This means that the programmer supporting your app needs to know all functions called by the current function in order to understand what the current function does. That's exactly the opposite of the purpose for writing functions - abstracting problems away so that you don't have to think about them! Now imagine 5 calls deep and 5 calls wide each. That's a hell of a lot to keep in your mind and a hell of a lot of mistakes to make.
"everything is a map" also seems to lead to using the map as a return value. I've seen it. And, again, it's a pain. The called functions need to never overwrite each other's return value - unless you know the functionality of everything and know that the input map value X needs to be replaced for the next function call. And the current function needs to modify the map to return it's value, which must sometimes overwrite the previous value and must sometimes not.
edit - example
Here's an example of where this was problematic. This was a web application. User input was accepted from the UI layer and placed in a map. Then functions were called to process the request. The first function set would check for erroneous input. If there was an error, the error message would be put in the map. The calling function would check the map for this entry and write the value in the ui if it existed.
The next function set would start the business logic. Each function would take the map, remove some data, modify some data, operate on the data in the map and put the result in the map, etc. Subsequent functions would expect results from prior functions in the map. In order to fix a bug in a subsequent function, you had to investigate all prior functions as well as a the caller to determine everywhere the expected value might have been set.
The next functions would pull data from the database. Or, rather, they'd pass a the map to the data access layer. The DAL would check if the map contained certain values to control how the query executed. If 'justcount' was a key, then the query would be 'count select foo from bar'. Any of the functions that was previously called might have ben the one that added 'justcount' to the map. The query results would be added to the same map.
The results would bubble up to the caller (business logic) which would check the map for what to do. Some of this would come from things that were added to the map by the initial business logic. Some would come from the data from the database. The only way to know where it came from was to find the code that added it. And the other location that can also add it.
The code was effectively a monolithic mess, that you had to understand in it's entirety to know where a single entry in the map came from.
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
It's more of a design exercise than a general recommendation. You aren't usually going to put a queue between all your direct function calls. That would be ridiculous. However, if you don't design your functions as if a queue might be inserted between any of the direct function calls, you cannot justifiably claim you have written reusable and composable code. That's the point Rich Hickey is making.
This is a major reason behind the success of Apache Spark, for example. You write code that looks like it's making direct function calls on local collections, and the framework translates that code into passing messages on queues between cluster nodes. The kind of simple, composable, reusable coding style Rich Hickey advocates makes that possible.