> You can use languages like Ruby ... in a way that matches this [Alan Kay] definition [of OOP], but they're not typically used this way
Oh, but it is. Writing in a message-passing style is how I was taught Ruby, with specific reference to the ideas of Alan Kay and the influence of Smalltalk. This describes a tremendous amount of my code and of those I work with. It’s a very natural fit for Ruby and far from uncommon.
Maybe that doesn’t apply to how folks are (initially) taught to write Rails apps, but that’s another can of worms.
This is describing issues and tradeoffs of OOP relative to FP and haskell. It conveniently side steps the fundamental issue with OOP as used in Java or C++ or C#. I agree with most of what the author says except that his OOP version is basically incredibly hard to read and not intuitive. However that's just my opinion.
The real big problem with OOP in context of the popular definition is the promotion of using logical operations that are not combinators. This makes OOP less modular.
class X:
int b
int c
int d
int x[100000] = {1,2,3,4,5,6,.......}
function addOne(){
return b + c + 1
}
The tradeoff with OOP is convenience. You have b and all the context of X implicitly accessible within the function but addOne cannot ever be used outside the context of X.
To reuse addOne means to recreate the entire context of X. I want a banana but in order to get the banana I must recreate the gorilla holding the banana and the entire jungle the banana is sitting in.
That's what most people complain about even when they don't realize it. In terms of the authors definition of OOP I don't think there's much controversy for that stuff.
I'm not sure I uderstand the point you are trying to make here:
> To reuse addOne means to recreate the entire context of X. I want a banana but in order to get the banana I must recreate the gorilla holding the banana and the entire jungle the banana is sitting in.
If 'banana' is dependent upon 'gorilla' then you're doing OOP wrong, or maybe you didn't need to reuse 'banana' in the first place and took some shortcuts while coding and now that you need you will have to refactor the code and extract that functionality into a new class.
Even in the case of inaccessible logic hidden into classes it's entirely possible to extract that code into, say, an utility class that can be reused across several classes. In this case the resulting code may resemble a combination of higher level OOP classes wich take advantage of lower level functional modules for implementing their behavior.
Yes, that's all true, but after even two stages of this you're in the kingdom of the nouns.
Note the argument is not structural, but philosophical. Yes, the two constructs are logically isomorphic, but to get composability with objects you must start twisting your brain in uncomfortable ways. I would concede there might be a minority of folks whose brain truly works like this. For the rest, it must be stockholm syndrome.
I think parents posters idea about reuse is that if you need to reuse it, you make it into an abstraction and hoist (copy/paste) to a superclass and make where you took it from a child class. On the one hand, it does make the reuse explicit ("explicit is better than implicit"). Or you broaden the member elements to be able to take a broader interface. On the other hand, this is how you wind up with enterprise java.
The analogy I'm using is too big. It's actually a famous quotation but it kind of blinds you to the fundamental issue.
Let me put it with a very primitive example.
class Jungle:
int bananas;
int gorrilas;
removeBananaFromJungle():
banana -= 1;
Literally the example above is the most fundamental example of a class you can write. A class with a method that operates on a member variable. You can't get any simpler than this. There's no alterative pattern here; this isn't some overly complex example you can fix with a pattern. This is objects 101, one of the simplest entities in OOP you can ever define and the problem remains:
I want a banana but I must initialize the jungle and the gorilla in order to remove a banana.
It seems like a good organization scheme because the banana and gorilla live in the jungle. But this is the issue with OOP. We can't predict the future and we can't predict how requirements will change or the perfect design.
So really the best way to program is to assume nothing. Don't assume either the gorilla or the banana are in the jungle:
int gorrila = ...
int banana = ...
map<string, int&> jungleStuff = ...
haveTheGorillaThrowABananaOutOfTheJungle(gorilla, banana, jungleStuff)
What if one day I'm called as a programmer to write a program about Gorillas on the Space station?
With the OOP version I have to create the jungle and the banana just to get the gorilla onto the space station. With the regular procedural version I do not. Basically 99% of accidental technical debt is the result of unintended groupings of logic and data promoted by the OOP pattern because people assume gorillas live in the jungle and they can never account for the fact that one day they will be on the space station.
I'm sure you can come up with some crazy OOP pattern that can account for both the regular Jungle case and the SpaceStation... but what if I start throwing all kinds of crazy requirements at you?
The spacestation now has to account for Ostritches, Gorillas can now live with ostriches in a house.... anything you can think of I will try to break your design with a new requirement (within reason) until you come up with a set of entities that can anticipate anything...
You'll find more and more that your classes will become smaller and smaller until basically you have an OOP pattern that can anticipate any requirement I throw at you:
class Gorrilla
int gorrilas
class Banana
int Bananas
class Jungle
map<string, int> stuffInTheJungle
class SpaceStation
map<string, int> stuffInTheSpaceStation
class House
map<string, int> stuffInTheHouse
class Ostrich:
int ostriches
Which is basically equivalent to this:
int gorrilas
int Bananas
map<string, int> stuffInTheJungle
map<string, int> stuffInTheSpaceStation
map<string, int> stuffInTheHouse
int ostriches
In short, any grouping of logic and variables through the use of free variables or classes reduces the modularity and flexibility of your program pointlessly. There is no reason why you should group logic together and you can never anticipate the future where your grouping will be found to be incorrect. To counter this problem you need to make your groupings smaller and smaller until each class holds one thing. A class that holds one thing is pointless, it's the same as just that thing itself without a class wrapped around it.
Thanks for the detailed explanation, I understand your point now, but I'm still not convinced that it can't be avoided/managed properly.
The primitive jungle class example you provided is actually just a condensed view of a jungle, holding counters of 'bananas' and 'gorillas', it only entangles two "counters" which is perfectly fine:
class Jungle:
int bananaCount;
int gorrillaCount;
removeBananaFromJungle():
bananaCount -= 1;
Now lets say we improve it to add actual 'banana' and 'gorilla' classes, it may look something like this:
class Jungle:
List<Banana> bananas;
List<Gorilla> gorrillas;
removeBanana(banana):
bananas.remove(banana);
In this example 'Jungle' is only an aggregate of 'Banana' and 'Gorilla', whose behavior is abstracted away in their classes. Moreover 'Jungle' only implements association logic. It's completely possible to define another aggregate class for a space station that reuses bananas and gorillas in a different manner:
class SpaceStation:
List<Banana> bananas;
List<Gorilla> gorrillas;
removeBanana(banana):
bananas.remove(banana);
You could even allocate bananas and gorillas separately in your program (or unit tests) and play with them if you desire, without an aggregation class managing them. The important thing here is to promote cohesion and avoid coupling.
Sure, you could say that both 'Jungle' and 'SpaceStation' are duplicating logic, but it's simply association logic at this point, nothing that prevents bananas and gorillas to live happily apart from each other. Furthermore, it's highly likely that as the these classes evolve they may handle removal of bananas very differently, for instance, in the space station it will need to be disposed into the void, and in the jungle the disposed banana may be used to fertilize the soil.
Why is changing an integer to a class an improvement? Isn't that like my last example where every tiny thing is encapsulated by a class?
>and play with them if you desire, without an aggregation class managing them.
This is my point. Within a class you have several entities that are managed by the class. I'm saying take away the management layer all together. Take away classes which is exactly what you said.
but you can simplify even further and take away the concept of classes all together.:
int bananas;
int gorrillas;
void removeBanana(int& bananas):
bananas --;
This isn't my point though. The point is in your designs you can never expect your methods to be defined like that.
What if for the space station but not jungle... if you remove a banana you have to remove a gorilla too? What do you do? You edit the space station method to account for that. Suddenly the house class requires this too. How do you account for that in the house without copying your logic? How do you account for that without refactoring SpaceStation? Refactoring is simple in these cases but more often then not OOP causes situations where the dependencies are much more tangled and complex.
Only if removeBananaAndGorrila(int& banana, int& gorilla) was already written outside the context of a class will you be able to reuse the concept and share it between two classes. So Why not remove the concept of classes all together? That way whenever something needs it, anything can use it.
Not to mention that your style requires something called dependency injection which greatly complicates code.
removeBanana in all OOP cases can never be reused outside of a class. You must always be redefining it and wrapping banana.remove. You can fix by taking this definition outside the context of the original class with a super class (inheritance) or you can just define it outside of a class all together with no restrictions. Why restrict the ability to reuse something? This is all a class does. Better to let the type parameters of a function to restrict usage.
> Why is changing an integer to a class an improvement?
Sorry, I should have said "extend" instead of "improve" which would have better captured my intent. My bad.
> Within a class you have several entities that are managed by the class. I'm saying take away the management layer all together.
This is a central point in our disagreement, we have different strategies for handling this kind of business logic. From my OOP background I'm more comfortable abstracting the management logic required for running a jungle in a 'Jungle' class, which is the whole point of OOP. You prefer another approach, and that's all right.
> if you remove a banana you have to remove a gorilla too? What do you do? You edit the space station method to account for that.
Yes, if you have a requirement that in Space Stations the number of Gorillas should be proportional to the number of Bananas, you implement that specific behavior in the Space Station class.
> Suddenly the house class requires this too. How do you account for that in the house without copying your logic?
Ok, if more classes have the same requirement maybe it's not a specific behavior, but another abstraction, which could potentially be extract into a new component, mixin or an utility class.
> So Why not remove the concept of classes all together?
It's a design choice. I feel that OOP, when used properly, allows me to manage complex abstractions more easily.
> Not to mention that your style requires something called dependency injection which greatly complicates code.
Indeed, it requires dependency injection, along with all of the other SOLID [1] principles. But from my point of view it helps to manage and reduce complexity, not increase it, and I have even written about it in my blog [2] :)
> Why restrict the ability to reuse something? This is all a class does
Classes are intended to encapsulate behavior/knowledge, not to restrict the ability to reuse something. Even though you bring valid points to the conversation, I think you have a few misconceptions about the objectives of OOP. Here's another article I wrote describing what I believe to be the fundamental concept of OOP [3]
Lastly, I'm not here saying that OOP is better than Functional Programming. They are different things, both valid in their own domain (and also combined together in the case of OOP, which benefits from borrowing a few concepts from Functional Programming).
> In effect, the major selling point of the “object”-style design here is also a major downside: you don't have guarantees about the specific representation of data, which means your programs can easily mix-and-match different representations, but it also means that your program can't make use of representation-specific knowledge in ways that are advantageous.
> There's another major concern as well, and that's that the specific choice of “object” representation can make a big difference in terms of what operations you can and cannot support.
The author is talking about representation of data, I'm talking about something different. I'm talking about scoping, grouping, modularity and free variables.
y = 6
f(x) = x + y
y is called a free variable and f is dependent on it. I cannot move f to another place because y will now be undefined. OOP is a more extreme version of this.
class X
y = 6
b = 8
f(x) = x + y
Now f can't even be moved to another place without b. If you want to move f you have to move b as well.
Combinators vs. free variables is the fundamental issue of OOP which this article writer effectively side steps with a unique definition of OOP where the method returns an entirely new object with differently defined methods.
It's ok. It's worth talking about the trade offs of unique definitions of OOP but I think the author needs to mention that this is also not a mainstream definition and therefore sidesteps all the mainstream controversy.
The other thing about objects like this, is that the members b, c, d, and x, are actually global variables, within that object. Where any other method can overwrite its value at will, unless you force all modifications through another setter method, which will perform the error checking and validation.
This is fine, when your object is just a toy object you learn in C++ 101, in school. But it becomes quite unwieldy, when working with actual real world business data. Unless you minimize the footprint of your object.
It's odd to write about objects in functional languages with no mention of Ocaml. Perhaps the Scala and F# users will feel the same way... There's much research & experience re: objects in the FP space that isn't considered in this article.
I have failed to fully grasp this article. Is this definition of objects as defined by the set of functions which work on them any different from type classes in Haskell?
I am not really getting what the thesis of this article is, and rather than try to hone in on that, I figured I'd give some thoughts on what OOP in functional languages means to me.
In terms of OOP, I primarily think of the paradigm being one that provides mechanisms for the encapsulation of both data and behavior and for relating these through inheritance or composition. The "only messaging" part of Kay's famous quote is too constraining in my view because it overly species the implementation details of the access of how one taps into an object's data and behavior.
For most people, their OOP experience has seen them use reference-based OOP languages like C#, Java, and C++. In such cases, there is a distinct line between that and what's typically thought of functional programming. This jarring difference is probably best noticed in F# where you have the referenced-based OOP and arrays brought about by C# and .NET/CLR interoperability and the functional layer of immutable-by-default discriminated unions, tuples, and records in the rest of the language. However, my experience in OOP has centered around value-based OOP, and this is where the line between OOP and functional programming is blurred.
This is because, in my view, one of the pillars of functional programming is that of immutable data. In a value-based OOP implementation, objects are merely immutable values, where methods are functions that take in the object and possibly some extra parameters and return a new one and possibly some extra data. It's just that classes and objects and the relationships that can be defined between them gives us the ability to encapsulate data and behavior and expose it in controlled ways.
F# records are very nearly functional OOP when viewed this way. F# records can contain member functions, where are "methods" in that they act upon records through dot notation but they do not mutable the record passed in. F# records can also implement interfaces, which is inheritance that defines relationships between specifications of behavior. It's just that F# records expose and do not encapsulate the data and, by some extension, methods.
I think value-based OOP has a place in functional languages. Records and tuples are product types that form new data by concatenating data, discriminated unions are sum types that form new data by or-ing data together through a flat hierarchy, and objects are types that form new data by collating data and behavior together with relationships formed between these types. If all one ever does to any of these is provide some functions, either defined externally as in functions operating on immutable types or internally in the case of member functions or methods, then this still satisfies one of the major tenets of functional programming, that being immutable and collections of functions operating on such data.
I think Kay's thoughts on message passing lies at a higher level or is an adjacent idea. One sees such message passing in actor models such as Elixir and Erlang, but those processes do not have hierarchical relationships like objects do, although they do have compositional relationships, nor do they provide mechanisms for strictly and cleanly defining data the processes are responsible for like what classes do. In my experience, coupling a system like Elixir and Erlang's processes with value-based OOP is a huge win, because these things are really different but complementary ideas. In my opinion, OOP being about message passing isn't really the case. It just so happens that Smalltalk married these two ideas (OOP and message passing) together.
A more general pillar of functional languages is referential transparency, which does not rule out mutability. It simply means that a function’s output is determined by its input alone. Have a look at Clean, uniqueness types and linear types in general, for instance.
Oh, but it is. Writing in a message-passing style is how I was taught Ruby, with specific reference to the ideas of Alan Kay and the influence of Smalltalk. This describes a tremendous amount of my code and of those I work with. It’s a very natural fit for Ruby and far from uncommon.
Maybe that doesn’t apply to how folks are (initially) taught to write Rails apps, but that’s another can of worms.