Reducing OOP to implementation detail features will yield poor results. The flexibility stems from an improved means of analysis in that types and entities can be identified more easily. By using classes and objects you're retaining flexibility because they can be interchanged; It's easy to create an object that pretends to be a function; it's harder to make a function retain state later.
The example is nice; it removes a lot of code but also removes the deferred execution aspect of the solution. Calling `countDominoTilings` will make it run in that instance; encapsulating the whole thing makes it able to pass it around and execute the potentially expensive `.count` method later. If applied properly, this can give you quite a lot of flexibility; for some memory and performance tradeoff; but that's the cost of abstraction.
Instead of looking for how FP and OOP are dissimilar, why not start looking for similarities instead? Objects are just higher order functions. Calling ctors is just partial function application.
(1) Interfaces / typeclasses / traits are good; inheritance is bad. An object is a family of partially applied functions with some shared state; this means tight coupling between them, rather often unwanted.
(2) Not having mutable state is the point. The more state you have, and the more code paths can mutate the state, the harder it is to reason about the program correctly. Examples of various degrees of hilarity / severity abound.
Sometimes shared mutable state is inevitable for performance reasons. It should be well insulated from other parts of the program then. Same applies to other effects, such as I/O. The standard approach is a core of pure functions with an outer shell of effectful / stateful procedures (as opposed to a mix of them).
While I'm on this soapbox, let me remind that Smalltalk was initially envisioned as an actor system, something like Erlang (hence "messages"), but hardware limitations did not allow to implement the original vision.
That depends. Inheritance makes it easy to break encapsulation (which is bad -- agreed). It can be hard to model "Is-A"-Relationships properly, but I wouldn't call it inherently bad. A circle isn't an ellipsis; but a chair is furniture. The quality of code stems from your quality of thought.
> this means tight coupling between them, rather often unwanted.
That depends on your design. Coupling is the whole point, you do objects because you want to couple. Fraction.reduce, Fraction.add, Fraction.subtract, Fraction.multiply. Why not have them as as a cohesive unit, all these functions must understand the details of fraction anyway. Why not couple them?
> Not having mutable state is the point.
I agree. But objects never force you to publish their state; that's bad education. Getters and setters should be avoided. The Fraction above can be made immutable easily, Fraction(1, 2).add(Fraction(1,4)) --> <Fraction: 3/4>, leaving the old ones intact.
But then, it depends on how you communicate state.
I believe `Connection.Open()` should return an Object of Type `OpenConnection`. I believe that state changes should be communicated by changes of identity (either a new instance or a new Type), but ideas like this are most often answered by waving torches and pitchforks in front of my house at night (figuratively).
Your OpenConnection idea might make sense in some abstract way, but one thing I know about connections is that they have a habit of closing. They will close without any notice to your programming runtime, because the operating system will do it. What happens to your OpenConnection object then? Well it becomes invalidated, and now you have a nonsensical object hanging around. So you read from it, get an error, and... now what? Replace it with a ClosedConnection?
> What happens to your OpenConnection object then?
It raises an exception. This interrupts the normal flow of things and asks you to deal with the problem asap. If necessary, the handling code then could try to reconnect or abort. If desired, it could return a closed connection to convey that state-change, so that calling code is made aware that it needs to reconnect first and can't just reused the now closed connection. You could revert it to a normal connection (a "Has never ever been opened to begin with"-Connection). Depends on whether your driver/adapter/underlying connection thing cares about a difference in initial connects or reconnecting. If the handling code can't deal with it, it can bubble the exception up one level.
Swapping a `Connection` for an `OpenConnection` isn't heretic by the way, such structures are described by the state pattern. Objects model state, Exceptions model events (state transitions) but the later isn't explicitly described in the original Gang of Four book that way. I just found that exceptions are very usefull for this, given that you react to them in only a limited scope.
Be aware that this idea is culturally dependent. In Java, Exceptions are often discouraged from being used in such a way (exceptions should never be used for control flow and only convey error cases), in Python it's normal to communicate state transition that way, e.g. for-loops watch for StopIteration exceptions.
Inheritance is a common-sense solution to the problem of creating something similar to an existing object, but different in some key aspects. It's programming by difference. Nothing bad about it until you start using it for type information.
>An object is a family of partially applied functions with some shared state
This is not true because of dynamic dispatch and because in non-sucky languages objects can interpret messages.
Nothing stops you from doing OOP with state that is immutable once initialized though. Likewise, nothing stops language designers for enforcing that in their OOP languages. You are arguing against a strawman.
I am not sure I understand your differentiation of explicit and implicit state. I believe you refer to Objects that change their internal values over time; in that calling the same function twice yields different results. Of course, such objects can be a pain to use.
Objects are all about making state explicit. Constructors connect different primitive values to a higher order concept, goal or task and checks invariants; The object's class gives the primitives a type which IS a form of state. A Year(2005) could guarante that the integer is > 1970; an int can do that on its own. 'hello@example.com' is not just a string; it should be an Email address and modelling this with an explicit class communicates state: that string is not just any string (all of Shakespeare's work in mandarin in base64) but a specific kind of string (an Email address). The mere existence of a valid object then communicates state, as do all types. I'd argue: Objects are all about making state explicit.
Still, it is really easy to abuse objects and make state implicit as you described above. I would argue that getters and setters are plain wrong to begin with, but that is a nother topic.
For example, if you do `Wallet.pay(amount)`, Ideally, you should get a new instance of a Wallet with the reduced amount, instead of reducing the internal credit variable. For many, this seems to be the wrong kind of modelling though, because when immitating the real world, you don't copy-clone your wallet physically when tipping a waiter.
Objects can and should be created immutable, but then, sometimes working with state is fine. For example an iterator (calling `bookmark = Boomark(book); bookmark.next()` a hundred times gives different page each time).
Imagine OpenGL; you send a lot of data to your graphics card and then tell it what to do with that data. Sending data is quite expensive. During rendering, you tell the pipeline what buffer to bind and what to draw at a single point in time (i.e. a frame). This is easlily represented by objects that keep track of the state. Your object-graph just remembers what buffers are bound. I, too have seen tangled and messed up designs; but I would still argue that thinking in state (my change does not go away) is intuitive for many people. Why not model systems that work like this explicitly? Shoehorning this into stateless pipelines can be quite difficult.
The problem isn't functions or objects (thats just fancy pointer syntax really) but that noone has figured out how to modell processes that work on symbols that change over time properly.
We won't fix this issue here; let's agree that we all like our state as explicit as possible as to avoid surprises and relieve or working memory.
I think your Wallet.pay(amount) example argues for a different conclusion than you arrive at.
I have a Wallet with $500 in it. I call Wallet.pay(100). In your approach, it returns a new Wallet with $400 in it. But I also still have the old, unmutated Wallet with $500 in it, which could be referred to by mistake (or by malice). That's probably not the best argument for immutable objects...
If you limit the scope of usage, the old wallet should be garbage collected as soon as you have the new one. If you don't want to rely on that, or the point in time when gc happens is important or you're dealing with sensitive data, then the environment should allow you to perform appropriate cleanup actions and for you to utilize a different means to control and protect the data.
As josephcsible said in a post parallel to yours, affine types can do this for you. If not, though... if you never mess up and keep a reference to the old one, you're good. I don't care when it's garbage collected; if there's no reference to it, it's not going to be used. But if anybody, ever, keeps a reference...
That's actually an argument in favor of linear or affine types. In your example, you'd still get a new Wallet from that function, but the compiler would keep you from accidentally using the old one afterwards. That way, you can't make that mistake, but still get the benefits of immutability.
I will admit that I don't know what "linear types" or "affine types" are. Your answer makes me feel like I am trying to convey an idea for which you have the perfect formalism / meta vocabulary. It might be that objects, or at least the way that I know how to use them in my favorite languges, are just a (potentially limited) implementation of said formalism.
I feel that objects offer a flexibility benefit though (which is why they often elude formal approaches) for the cost of purity.
Rust is probably the most mainstream language that uses affine types. Once you call a function that takes ownership of a value (e.g., `drop`), the compiler won't let you use it anymore after the function call.
Is that true though? You can pass the function itself. If your interface is a function that takes no parameters and returns an int, just wrap this function with another function that takes no arguments and moves the arguments into a closure.
Doing it in a class is forcing these assumptions on your user - that they need this added complexity in all of their uses, and will make for unwieldy code when they don't, or even more unwieldy code when they use a facility that doesn't satisfy the interface you decided on. Let your user be responsible for deciding that and wrapping your code for whatever use they want to do with it - if you write library code (library and not framework), it should be as simple as possible to allow flexibility for the user.
def adder(x):
def add(y):
return x + y
return add
add_five = adder(5)
add_five(10)
How is that different from:
class adder:
def __init__(self, x):
self._x = x
def __call__(self, y):
return self._x + y
add_five = adder(5)
add_five(10)
Did you have something like that in mind? Do I understand your idea correctly?
They both allow you to achive the same thing. The syntax doesn't matter, they both produce the same result (in Python, that is, which is a bit unfair, because there, functions are objects to start with -- but even if this was not the case, then the object would require namespacing which is negligible). It depends on whether you want to `think` with a closure or not.
I propose that differences in OOP and FP on this level are irrelevant, but that the magic lies in the naming chosen here. An "adder" is something that has continuity and identity, and the calculation is performed later. I don't care about how it is constructed. There is nothing wrong with just calling add(1, 2) -> 3, but objects give you this deferred stuff for free (sorry, not free, for the price of some memory).
The difference I was aiming at is that you (a library author, whether public or internal) shouldn't implement adder, you should implement add. And let your caller implement adder using add, if they require it. If you only expose adder, you limit possible uses for your library.
If I'm writing a library for public consumption that performs a non-trivial operation (the only time I would write a library for public consumption), I absolutely do assume I know better than the calling code.
I feel like most of these threads boil down creating stawmen of different situations in which FP or OOP fall down, and then declare that case the most essential problem in programming.
Programming is hard. Full stop. The most essential problem is programming is that most of the people engaging in it are not adept at it enough to avoid painting themselves into one of many, very hairy corners, regardless of what programming paradigm they are using. Excel users, coding boot camp graduates, PhD candidate scientists, mechanical engineers, computer scientists who never learned how much Dijkstra liked to troll people and took him way too seriously.
You write the code that does the job. That's not "does the job" in the sense that people who say things like "good results can be made in any programming language, including PHP" mean. That's "does the job" in the sense that it also doesn't blow up when you drive two motorcycles over it after having only tested it with trucks. Bad results can be written in any programming language, even Rust.
And the only operative difference between those situations is there experience and study of the programmer. You don't get great programs by choosing particular languages or programming paradigms. You get them by hiring great programmers. Who then have specific languages and paradigms that they use for different situations.
If you know better than the calling code, you're probably writing a framework and not a library :) A library should not be limiting, and should be easy to adapt for use with other code. Frameworks are highly opinionated so it's ok for them to only work in specific use cases (in exchange, they offer increased ease of use for those cases).
For example, the Golang Echo HTTP framework supports generating Let's Encrypt certificates using AutoTLS mode. It's not very configurable (and for my use case, that was a limiting factor), but because it was built on top of the autocert library, I was able to use the library directly and make it work with Echo in my use case. If the library was too opinionated, I would've had to fork it to use it in my use case, which was not anticipated by Echo's author.
> it's harder to make a function retain state later.
I do not agree with this kind of argument. If you need to transform the function into an object later, you can just do it later. Even in a huge codebase it does not take much time to do it.
The `what if [...] someday` argument only leads to unnecessarily complex and expensive code, and most of the time you will never actually need it.
It's not so much about "I need that later", but to start thinking about a program differently to begin with. If you think about the things that "are" rather than what should happen in what order, you get the chance to rearrange everything and do some optimizations that an eager processing might prevent you to do. Most of the OOP power comes from uniformity, which many systems break unfortunatelly. When you work with a system in which everything is an object, you can start to relax a lot, although everythin is conceptually slower. Most software doesn't even have to be that fast. If it has to be, feel free not to use Ruby.
The tradeoff is most often performance versus comprehensibility. I'd argue in favor for the latter. Of course, ergonomics and ease of use are hard to measure (but not impossible, although empirical studies are quite difficult to do and expensive), but the tradeoff is similar for all higher level languages. Consider the overhead for a class `Year`:
Insane! Expensive! you might say. All it does is encapsulate some integer! But I'd take that little overhead over the scattered insecurity of my colleagues every day, who in every calling method will do the same "if then that else"-check for the year range over and over again, when they are handed an int and need to find out what's in it. The class provides locality for my concern that I only ever want to deal with valid years.
When, in your system you find a year-typed object somewhere, it is guaranteed that this is valid. This creates peace of mind, which is way more expensive than RAM.
It seems to me that this is mostly a design problem. Why would you need to check this data everywhere?
Checking the validity of the data is only necessary once. In a web context, this is the responsibility of the controller. Only once the data is sanitized should the controller inject it into 'anything else'.
Sanitizing data is not the responsibility of a model/service/repository/view/anything else, and trying to do so indeed leads to a lot of bugs and headaches.
Having this kind of object that checks your data and may throw exceptions anywhere on the code only augments the failure surface of all your codebase by adding unexpected exceptions.
Comprehensibility is in no way related to using objects. Wether you choose an integer, a class or a subtype of integer does not make anything more readable, it all depends on the quality of the naming. A var named `year` or `startYear` will always make the code more readable than a var named `start` or `begin`, whether it contains an object or an integer is irrelevant.
> Checking the validity of the data is only necessary once.
Exactly, which is why a class is the perfect singular location to place it. The object itself is just a pointer, the method doesn't get copied around, so object appear to be the perfect method to localize code.
> unexpected exceptions
True, exceptions introduce a communicative issue, but so does returning in-band values.
For example, what should the result of
open('file.txt').read()
be, when the current user does not have permissions to read file.txt?
I wouldn't argue against any of them, although I have my preferences of course. I like the exception model here, but you are right, rare exceptions, communicated poorly can be surprising and painful.
> Comprehensibility is in no way related to using objects.
On itself this statement is false, ... (hear me out)
> It all depends on the quality of the naming
... But this makes me understand what you're trying to say, and I 100% agree with it. In fact, I conducted some experimental research on identifier naming: https://link.springer.com/article/10.1007%2Fs10664-018-9621-... (Sci-hub or I can provide a preprint).
You are right in that objects and their use don't automagically turn a codebase in a field of readily availabe knowledge. Many mechanisms applied in OOP languages really work AGAINST comprehension (for example, buried exceptions originating from deep within an object graph). But still, objects are tightly coupled to comprehension, even historically speaking. They were first used to make it possible to model physical simulations without requiring users to know much about computer architectures (Simula 67). Objects are meant to "model", that is, symbolically represent concepts, entities or physical things in such a way that they might show agentic behavior. This is fundamentally different from having stupid data, smart functions but actually a completley different means of "Erkenntnis" (translates to "insight", but is more accurately conceived as "Epistemology"). The relationship between objects and readability / comprehension is complex, but I wouldn't call them "in no way related" (OOP can break comprehension, but it was invented to improve it. Irony.)
Also I would, again, like to second your words: Identifier naming might be the most imporant aspect of readability and comprehensibility.
You only lose deferred execution in this case because it didn't go far enough away from OOP and toward FP. For example, since Haskell is a pure functional language, it can be lazy by default, meaning the function-only version transliterated there will have deferred execution.
Why not make max or pow an object then? If you can model something as a pure function, then it should be a function. You can always add memorization later, if needed. OOP is fundamentally different from FP, objects are not just higher order functions, they are also coalgebras. There is no need to force every peg into a coalgebra hole.
`max` and `pow` can either run eagerly (as functions) or they can run lazily and allow for interesting lazy things. For example, if you put them into a lazy object graph, you could perform optimizing measures.
Check this out:
1/2 * 2/1 == 1
Would you actually calculate each intermediate result of that expression? Or would you just reduce the fraction? Should 1/2 be exectuted and yield 0.5 immediately, or could it be useful to have them hang around some more? FP vs. OOP is the difference of understanding / as "Please divide 1 by 2 now" vs "That IS a fraction; one half".
> There is no need to force every peg into a coalgebra hole.
True, but how awesome would it be if I did that anyway :D
Regarding `max` and `pow` as objects: Yegor Bugayenko argues for a similar thing in his book "Elegant Objects".
You can get the same effect if you model the expression as a free algebra (ADT). You can then simplify the terms in the same way.
The paper: "The design of a pretty printing library", by Hughes, explains this nicely.
The main difference is that algebras are more natural in FP and coalgebras in OOP, but they can mostly do the same things, just a bit differently. Actually you can also do algebras in OOP as well (visitor pattern) and coalgebras is FP.
Basically my point was that you should use the simplest abstraction that gets the job done. Even if you implemented max and pow as objects, for your specific use case, you would probably just call the pure functions inside.
The example is nice; it removes a lot of code but also removes the deferred execution aspect of the solution. Calling `countDominoTilings` will make it run in that instance; encapsulating the whole thing makes it able to pass it around and execute the potentially expensive `.count` method later. If applied properly, this can give you quite a lot of flexibility; for some memory and performance tradeoff; but that's the cost of abstraction.
Instead of looking for how FP and OOP are dissimilar, why not start looking for similarities instead? Objects are just higher order functions. Calling ctors is just partial function application.