I think this description of what OO is, is flawed and makes people talk past each other:
> • What is OO programming?
– a programming paradigm that uses "objects" – data structures
consisting of datafields and methods together with their interactions – to
design applications and computer programs.
(Wikipedia)
It isn't that they are together, it's that the core entity of the language used to express solutions centers around identity over state or values. As in: two objects are different even when the fields are all equal, and also: two objects can be the same even with different field values.
If you are going to object to this, I ask that you first contemplate whether or not you have programmed in any other way first, because as a fish, reasoning about what water is can be difficult.
i am going to object to this; i've programmed in, among other things, prolog, ocaml, basic-80, c, the unadorned λ-calculus, bicicleta, and qfitzah, the last two of which are languages i designed and implemented myself and which have no mutability. moreover i don't think anyone would claim that basic-80 or c is object-oriented
so i feel secure in claiming that i am more a walrus than a fish and therefore qualified to opine about water
i do agree that identity and hidden mutability are important aspects of the oo view of the world, and that the author of these slides is off-base about what oo is. but i think that the essence of oo is that computation is done by sending messages to objects, but sending the same message to different objects invokes different code. this feature is present without mutability in bicicleta, and in abadi and cardelli's untyped ς-calculus, which it's based on; the ς-calculus was invented to formalize the essence of object-oriented typing
the original intuition behind oo was to decompose a computer system into smaller computer systems, like a computer network; each object in the system was in effect a tiny computer, with its own private code and state. this was inspired by alan kay's undergraduate degree in biology, which mostly studies organisms made out of cells, which are basically tiny organisms. and in that formulation, obviously hidden mutability is the default, because you can't prevent a computer from changing its internal state as a result of a packet you send it; and mutability generally means identity is important
but even more central to this formulation is the idea that different objects can behave differently in the same circumstances
obviously none of this has anything to do with memory layout! except very indirectly
This is it. OOP is about building sub programs with their own state and behaviours, yet implementing a common interface so they can still effectively communicate to each other.
If you boil it down to the essence, this means branching logic is encoded via a function pointer. Instead of manually switching to the correct behavior using other branching mechanisms like if or switch.
This is what polymorphism is about. It's pattern matching.
An interface is like an Enum to which you can add entries after it has been defined, and the behaviour for that specific variant naturally then has to be encoded inside the variant itself.
Would it be far fetched to say object oriented programming is inspired by nature/physics?
And thus, "more natural" than, let's say, procedural programming.
Animals can be modeled nicely with objects/classes.
Physical objects do have internal private state and public interfaces. Also in nature, usually, you have multiple similar objects that work the same but have different internal state.
That might have been what Alan Kay was going for with his "cells of an organism" thing, yes.
But even if you're not inspired by biology, it might just come up "naturally". Separating your data into structs, then operations on those structs, and not wanting to deal with all kinds of bookkeeping all the time, letting things manage their own - separation of concerns, aka single responsibility principle.
I think automatic memory management might be a big example of this. Who wants to bother with this malloc/free stuff all the time? Who wants to keep track of ownership everywhere? Instead, have a dedicated subsystem handle it. That allows a higher signal to noise ratio in all the rest of the code that would otherwise be polluted with malloc/free. It also removes the entire class of bugs that can happen when you inevitably forget either of those.
But having memory management in and of itself isn't "OOP". It's just an example of one of the principles that people tout when they talk of "OOP". Those principles can and do apply broadly.
Was Alan Kay thinking of all this? I really don't know. All I know is that the dude digs biology.
I think more that messaging was the fundamental part. Modularization and composition is pretty common in most paradigms. I actually looked this up the other day for a different comment but this is Alan Kay's own words on the subject:
It's a concept I really like and use quite a bit in game development at the inter/intra-entity communication level even if below that I'd pick a more performant substrate.
> OOP is about building sub programs with their own state and behaviours, yet implementing a common interface so they can still effectively communicate to each other.
This doesn't seem like a sufficient definition, as closures are also sub programs with their own state and behaviors that implement a common interface (the function signature), and you'd have to stretch the definition of OOP to the point of meaninglessness if that would be sufficient to qualify as OOP.
You nailed it. Object programming was never about the low-level details like inheritance; it was always about managing state and sharing behavior (just like function programming!). Object programming looks at things from the perspective of the object; each object has some state and some behavior it can respond to when sent messages. Objects with compatible behavior can be substituted for each other (this is the essence of behaviorism[0]). This is symmetrical to function programming's pattern matching where values with compatible structure can be substituted as arguments to a function (this is also why I firmly hold the belief that Lisp and Smalltalk are symmetrical languages). Languages like Haskell even use things like "Integer a =>" which are basically object programming interfaces!
We don't disagree at all by the looks of it. I was objecting (lol!) to the description of OO as code and data together. _Together_ doesn't carry the necessary weight here because a struct and a module of functions in the same file is also code and data together.
It's that the receiver of those messages have an identity that defines it.
Inheritance and polymorphim is of course central to OO, but it's not defining characteristics imho because inheritance is "module arithmetic" and polymorphism has plenty of flavours.
(I am in no way trying to have a go at OO either.)
it is astounding to me that my comment has three separate replies saying variants of 'i agree completely' and then disagreeing completely; was my writing really that unclear
You've hit the nail on the head, and I think the replies below aren't getting your original point. Which I think is actually your point :-)
The defining feature of OO approaches is in shifting the emphasis in sw development into the concept of identity-bags. All the other stuff about message passing, encapsulation, inheritance etc is just window dressing and implementation details of this core concept of identity-classification in software systems.
I like how the "Out Of the Tarpit" (2006) paper breaks this down into the notion of "intentional" vs "extensional" identity, and I think this is a great model.
For them, OO is all about "intentional identity" in that we create identities (objects, components) in our systems and move attributes and behavior onto them. OO shifts the analysis of software to identifying objects, components, services, and then makes software development all about the taxonomical management of them. In this model of software development, it's all about identifying components and then shuffling the data and functionality "into" them and managing the flow between them. (FWIW the paper identifies this way of thinking as one of the prime culprits in unnecessary complexity in software systems, and I agree with them.)
The contrast is "extensional" identity, where identity in the software systems are emergent from their attributes. In this box we can put the relational data model, and its cousins in logic programming (prolog, datalog etc). In those systems, we are not concerned with "creating" identities and classifying systems/components/objects, but instead declaring the raw attributes -- tuples, facts, relations -- only. Queries then produce, at runtime, a multitude of results from permutations of those attributes. And, as you point out, equality/inequality is defined by the value of a tuples attributes, not by the object it lies in. This is crucial.
There's a couple generations of software developers that are so immersed in the OO/intentional-identity way of thinking that they don't see that it's not intrinsic to software development, but actually a mental framework that was fostered through the 80s and 90s. It was an attempt to manage complexity and structure in software systems and also to create a marketplace of re-usable components. But I think it's actually failed at that.
Most classes, objects, components, microservices end up failing in two respects: they end up reflecting the company's org chart more than anything else, and .. worse .. the bulk of the code and complexity ends up being how one moves data between all these (programmer created) systems rather than what the actual data is. The relationships (lines) between the boxes becomes the focus instead of the stuff was shoved into the box, and the whole behavior of the system becomes difficult to reason about.
And when developers encounter systems that don't work along this model -- relational databases, functional programming, logic programming -- they often scratch their heads, resist, or wrap them up/confine them in OO/component type structures... damaging their usefulness.
A great, and often under-appreciated point is - as put by Guy Steele - "We find that the notion of side effect and the notion of equality (object identity) are mutually constraining; to define one is to define the other."
With that in mind, I do believe that objects, as in encapsulating some kind of side-effect, is a very good model. But it is not a replacement for everything - a more data-oriented approach makes much more sense for some other parts of a program. Multiple views are absolutely possible within the same language/program, and should in general be the preferred way.
What I would like to see is a system where object-orientation is emergent from a more set-oriented/relational model. I haven't put my finger on what exactly this is yet, but maybe it's something like "methods" really being a kind of predicate-dispatch over matching tuples.
So a "class" (and object identity) is really telling the system: when you see attributes X, Y, Z bundled together as a result of a query, here is a set of behaviors that you can apply on those tuples.
I get your point, but ... updates on tuples are definitely a "thing" in a relational system... and the set of connections and their attributes and state can be modeled in relations quite easily... Not sure I see a conflict.
Fair enough, though I think a functional approach (you create a new set from the old, with one field changed), and somehow “committing” it back is fine, and no need for side effecting then (besides at the boundary).
Maybe structural typing is somewhat similar to what you think of? Probably typescript, maybe go are the most well-known for this out of the statically typed ones, but clojure is also all about every “object” being a key-value map.
Yeah, a functional approach is fine. In reality you can think of an "update" to a tuple in a relation as a "replace this relation-valued variable with a new one where this tuple has been modified." And so mutations to base relations ("tables" in SQL parlance) just means changing the value of that relation in in the global shared, named base relation list. And if you use a persistent data structure behind the scene, and do CoW etc, you can even probably make this relatively efficient...
And I don't think it's so much structural typing, as it is defining method/function dispatch off predicate matches. More.. structural values? Clumsily:
Disconnect(connection_id) = with project ConnectionState CS where CS.Id = connection_id {
if CS.state != 'connected
Raise('not_connected)
else
CS.state = 'disconnected
}
Mutating a tuple in a relation doesn't really make sense per se, because a relation is just a subset of the cartesian product of its attribute domains, and only value-based semantics apply to sets. What does make sense is identifying a primary key for a relation, deleting a tuple, and inserting another tuple with the same primary key but different values of its other attributes. In practice, of course, this is equivalent to in-place mutation of identity-objects corresponding to unique values of the primary key.
Indeed, we seem to understand each other. A very well put post of you!
I've come to appreciate these difference in later efforts to learn functional programming as a way of giving myself more tools. But for the longest time, it was very difficult for me to "get the boxes right" for what things go where. What is a module, an object, a function, a method, etc, coming from the object oriented universe.
I still have the urge to "create objects" when I conceptualise solutions and _imho_ this isn't always a good path.
These days OO exists within my horizon of tools for solving tasks. Before, it was the water, if you will. Some problems have a want for objects, others don't really.
The site builder, Uriel [0], was like those of us in his circle, strongly opinionated.
Sadly he suicided in 2011 with Sodium Pentothal he bought mail order from China. My final Skype message to him was "goodnight, don't drink the pentothal!" and Skype used to show it to me under his username every time I logged in.
if you mean that ridiculous "gcc/pdf/web browser is ridiculous, use tcc/txt file/curl" bullshit page, then I can't take you seriously. Feel free to use a pocket calculator instead of your current device, I guess, that's not bloated.
And also Smalltalk, I believe. It appeared ~15 years earlier than Erlang. The idea of holding encapsulated state and receiving messages is its core concept, AFAIK.
It does make a lot more sense if messaging is asynchronous, because the metaphor breaks down immediately if sending any message requires the sender to freeze until a reply comes back.
I don't think anyone would "want" CPP. It feels like it's multiple layers of "this is how we've always done it" interleaved with some really good ideas (STL).
Just because someone coined the term, doesn't mean that they get to define it. The industry absolutely doesn't use OOP in any way similar to what Alan Kay does, and the former is the de facto correct usage.
It's also worth pointing out that as smart and important of a guy as he is, Alan Kay has not been employed as a software developer for a very long time.
What OO has become in the industry is a product of practical concerns encountered by people working in the field. Can't say I like it much, but working in this industry for 25ish years, I can understand the pragmatic "whys" of every step of language and tool design fairly well.
I find "modern" OOP concepts apply pretty well to graphical user interfaces. I believe that a good UI has a consistent and relatively strict hierarchy, with few "unique snowflake" elements. Even such a divisive idea as multiple inheritance often makes sense in UI, i.e. there could be an element inheriting low-level behavior (HTTP/API call, etc) from a button and a high-level behavior (style/presentation, etc) from a visual class.
(In a way, even CSS is kind of OOP, albeit with very confusing overriding rules.)
When writing generic backend code in a language like Java, I often found fighting against OOP because the business logic is not necessarily mappable to "dog inherits from animal". And I can't change the business logic, so OOP has to give in. But when developing user interfaces, I am usually in more control, I am actually designing it, not observing directly from the business logic. And OOP forces me to have a more consistent implementation, which is beneficial for the user, because consistent UI makes it easy for people to get used to it, find the underlying logic in it, and start relying on it. If all links on all web pages would inherit from normal HTML href link (like it was designed in the beginning), we wouldn't suffer from those weird JS web apps where you can't open a link in a new page for example.
Inheritance is usually not the solution; composition and delegation is. Unfortunately, the current landscape of popular languages have equated "object programming == inheritance" in people's minds. The fact that these languages make inheritance easy but provide no syntactical or semantic support for composition and delegating to inner objects compounds this problem.
> there could be an element inheriting low-level behavior (HTTP/API call, etc) from a button and a high-level behavior (style/presentation, etc) from a visual class.
This becomes very messy very quickly, and you would need to clearly separate the concerns to make them non-expressible on either side. CSS and JS make this possible by being two different languages, one Turing-incomplete (in usual scenarios).
I think of inheritance as just being a workaday implementation detail to facilitate code reuse in some scenarios. Polymorphism ("many forms") is more related to the concept of interfaces, but as you touch on has been equated with inheritance in a lot of peoples' minds.
Then how do you avoid duplicate code without using inheritance? Other solutions like placing shared functions in "helper" classes are far worse from a complexity and maintainability standpoint.
"helper" classes are a symptom of an object-obsessed programming language, a language where functions can't live in a module, they must always exist in a class. Especially if it doesn't make sense for a class to hold them.
You avoid duplicated code by making generic functions, putting them in modules, and making your objects accept them as parameters.
I guess it's like this – implement interfaces, and common code comes with it when you inherit (then override to modify behaviour). A common interface really should have common code so this is appropriate. What is wrong is to inherit from an object with a different interface just to get your hands on the code - that might seem obvious but when OOP started to be used in industry, that was acceptable or even recommended practice. That malpractice was formalised away by Barbara Liskov with the Liskov principle. I'm pretty certain you're doing the right thing already so it was a clarification rather than a criticism.
In Delphi, this is my preferred way. I use interfaces[1]for the public-facing stuff, but I might use inheritance to implement the interfaces for code reuse and ease. I of course also use delegation a lot, but for some things I feel inheritance is a better fit.
Regardless, the inheritance remains an implementation detail.
> make inheritance easy but provide no syntactical or semantic support for composition and delegating to inner objects compounds this problem
I admit I would really like a 'delegate function call to <object>' construct, but excuse me for being dense, what composition are you referring to? Other than inheritance and delegation, what else is there[1] , and what would a suitable construct supporting it in the language look like? TIA
[1] (there is functional-style composition, but that's perfectly well supported in many decent languages like Scala)
Kotlin's delegation feature is useful sometimes, but OOP inheritance still crops up more often.
One reason is that inheritance is strictly more powerful. When you delegate (compose), you can't modify the inner working of the object you delegated to, you can only wrap it. Often that's not sufficient. You want to actually customize the implementation in some way. OOP's inheritance is ideal for this, because the superclass can clearly define chunks of functionality that subclasses are allowed to either (a) replace entirely or (b) wrap or (c) provide (abstract methods).
This is a very flexible approach. With mere delegation you can only wrap, and only for method calls that originate outside the object, and only for public methods.
It's also clean. The distinction between what you must implement, what you can implement and what is for the user is defined in the type system and enforced by the compiler. Additionally you separate the interface for customizations from the interface for users (protected vs public), which keeps documentation and auto complete properly focused.
When combined with dependency injection and code generation it also allows third party frameworks and libraries to enhance the code you write in various ways, that remain mostly tucked out of the way but can be revealed in an IDE in an instant (and a good IDE will show visual hints that there are such customizations in effect).
The final advantage is that in some languages the implementation is highly optimized. On HN you'd be forgiven for thinking nobody writes virtual method calls since 20 years ago but in reality virtual method dispatch is so common in real programs that it's heavily optimized by both CPUs (sophisticated indirect branch caching) and language runtimes (PICs in fast VMs like Hotspot or V8). Composition is less well optimized.
Inheritance gets an overly bad rap on Hacker News, despite how prevalent it is. I think this is because it's so flexible you can easily make a mess with it, and some people have. Also the decision of Java to make all classes and methods open by default means overriding is often used as a hacky way to hot-patch libraries in the field, rather than as part of any principled overall design. Kotlin reverses that decision to some extent and so classes and methods are all final by default, unless a compiler plugin overrides that.
I also suspect the move to web development has had this effect, because in most classical OOP toolkits you are allowed or even encouraged to extend the UI by subclassing pre-existing node types. So you can make a custom button by subclassing Button or whatever. HTML is implemented using classical OOP designs, but mostly for implementation reasons that isn't exposed to the web developer who instead is expected to customize the controls purely through CSS and setting properties. If you want to make a custom button widget, well that's too bad, better learn React or something similar. So people just aren't exposed to the situations where OOP is useful because browsers weren't designed for UI, and the memory of why it was originally developed atrophies.
It's all Turing complete, so of course you can implement the same thing as protected methods by using callbacks. When I say power here I don't mean in the sense that you literally can't do it any other way, I mean it's more convenient and natural.
C++/Java/Kotlin style OOP is basically about putting common design patterns into language syntax. You could do it all with C, but it's easier if the compiler does it for you. For example Kotlin has "data classes" and Java now has "records" which are in both cases classes, but they sacrifice inheritance in order to auto-generate more boilerplate for you. That's still OOP though.
> One reason is that inheritance is strictly more powerful. When you delegate (compose), you can't modify the inner working of the object you delegated to, you can only wrap it.
It's more powerful, sure, but in the same way that goto is more powerful than if/for/while. In theory some languages give you the tools for a superclass to expose a stable interface to its subclasses (ish - even Java doesn't give you a way to specify "subclasses may read but not write this field", for example), but in practice they're rarely used (Wicket is literally the only library I've seen to offer an effective inheritance interface); the overwhelming majority of the time inheritance ends in fragile base classes and the resulting bugs.
Which is perhaps inevitable; the trouble with giving you the tools to change the superclass's behaviour in unintended ways is that it results in people changing the superclass's behaviour in unintended ways. If you have a locked-down inheritance model where classes and methods are final by default then maybe you avoid the fragility problem - but by the same token if subclasses can only use extension points that were deliberately exposed then you're probably not gaining much flexibility over what you could do with composition.
> The final advantage is that in some languages the implementation is highly optimized. On HN you'd be forgiven for thinking nobody writes virtual method calls since 20 years ago but in reality virtual method dispatch is so common in real programs that it's heavily optimized by both CPUs (sophisticated indirect branch caching) and language runtimes (PICs in fast VMs like Hotspot or V8). Composition is less well optimized.
Shrug, as the language design consensus shifts, the implementation optimizations will follow. Composition should ultimately be easier to get better performance out of, because it's more constrained (in particular, knowing that a given class or method is final means that you can optimise that class or method much more aggressively).
> So people just aren't exposed to the situations where OOP is useful because browsers weren't designed for UI, and the memory of why it was originally developed atrophies.
AIUI the association between OOP and GUI is more of a historical coincidence than a deliberate design. Though I agree that they do seem to fit together well.
> even Java doesn't give you a way to specify "subclasses may read but not write this field", for example
I think I know what you mean, but you can easily define a private field with a protected get method which amounts to the same thing.
In Kotlin there's no distinction between properties and fields, so you can write:
open class Foo {
var thing: String
private set
protected get
}
to express that.
> in practice they're rarely used
I'm not sure I agree. Most Java libraries use it, and it seems to work fine. Mobile UI has used it for many years and that's an absolute ton of code. Game engines use it, games are a huge chunk of the software industry. People would use it a lot more if the web worked differently or if we had better non-web distribution systems on the desktop.
> if subclasses can only use extension points that were deliberately exposed then you're probably not gaining much flexibility over what you could do with composition.
You can certainly do everything that inheritance allows without support for it in the language, but that doesn't say much. You can also do OOP in C, people do, but it's generally more convenient to have compiler support. Used properly, inheritance can certainly lead to much more convenient APIs and code without being prone to misuse.
As an example, one of the issues I have with Jetpack Compose is that if you want to customize the controls in any way you have to wrap them. There's no way to say "I want a MyButton that's the same as Button except in this one aspect", because OOP is gone. You have to define a totally new MyButton function/composable that delegates to Button. In JS it's just about tolerable due to the dynamic typing and related features (e.g. spread operator), but in Kotlin it doesn't work despite relying on compiler plugins that change the language's core semantics. You end up with huge screenfuls of boilerplate which are just forwarding function arguments from one place to another, and if you upgrade to a new Compose do you get the new features automatically? No! Due to its browser heritage React conflates widget libraries with theming, so you've had to wrap all the widgets to get anything beyond the basic Material Design look, and that's manually generated boilerplate so you'll have to go and plumb new properties through by hand. There's also no nice way for the authors of Button to define properties that are only intended for customizers vs users because there's no concrete notion of customization at all. Ugh.
Now compare to an OOP toolkit:
class MyButton extends Button {
...
}
You upgrade the base libraries and MyButton now has the new features automatically, no code changes required.
> knowing that a given class or method is final means that you can optimise that class or method much more aggressively
Virtual method calls are treated as final when possible in good language VMs anyway, so it makes no difference. They're only emitted as virtual when there are actually multiple receivers observed at that specific call site, which would have to be implemented as a switch{} in composition, so again, you don't gain anything but do lose language convenience.
> you can easily define a private field with a protected get method which amounts to the same thing.
It's not though, a method is significantly more cumbersome. People get lazy and make it a protected field, and probably they themselves remember that the subclass mustn't change it and it's only the poor next person taking over maintenance who gets bitten.
> Most Java libraries use it, and it seems to work fine.
I mean specifically offering a restricted interface for inheritance by making extensive use of "final", making unstable fields and methods inaccessible etc.. In my experience libraries are rarely careful enough about that, and upgrading a library that you've subclassed from is usually a fraught process. (Wicket is literally the only such library that I've ever been able to fearlessly upgrade)
> Used properly, inheritance can certainly lead to much more convenient APIs and code without being prone to misuse.
I'm genuinely not convinced. I think if you have interfaces and delegation support, then a well designed non-inheritance API (which might involve e.g. splitting what were single classes in the inheritance version into two) can be just as convenient.
> You end up with huge screenfuls of boilerplate which are just forwarding function arguments from one place to another
Isn't the whole point of delegation as a language feature to avoid that? Can't you just say "delegating this interface to this member" and then all the methods you didn't specifically define get forwarded?
> You upgrade the base libraries and MyButton now has the new features automatically, no code changes required.
Or it breaks, because the base library changed in a way you didn't expect / didn't take into account the way you've subclassed it.
> Isn't the whole point of delegation as a language feature to avoid that?
Yes but Kotlin delegation is implemented for objects and interfaces, not functions. Compose is React inspired so it uses functions with keyword arguments for everything, so if a new keyword argument is used, you have to manually forward it.
> Or it breaks
It can happen for any kind of API upgrade, especially where you can customize large amounts of complex logic. I don't think it's unusually worse for inheritance based designs. Obviously that's predicated on there actually being a design, but that's not a problem unique to Java (e.g. C libs that export non-stable symbols is a perennial issue).
> Unfortunately, the current landscape of popular languages have equated "object programming == inheritance" in people's minds.
Because OOP is somewhat equals to inheritance (or more strictly late binding + open recursion + self-polymorphism, inheritance is just one way to achieve that).
"delegating" is completely orthogonal to OOP, you can have delegates in any modular language, like ML. Saying "you need to use more delegates and less inheritance" basically equals to "you need to write less OOP code".
module type Repo = sig
type t
type user
val get_user : t -> name:string -> user option
val get_all_users : t -> user list
end
module LoggedRepo (R : Repo) : sig
(* Resulting module is of type Repo plus create function *)
include Repo
val create : delegate:R.t -> logger:Logger.t -> t
end = struct
type t = { delegate : R.t; logger : Logger.t }
type user = R.user
let create ~delegate ~logger = { delegate; logger }
let get_user repo name =
Logger.debug repo.logger "Get user by name";
R.get_user repo.delegate name
let get_all_users repo =
Logger.debug repo.logger "Get all users";
R.get_all_users repo.delegate
end
In languages like C it would be harder since they don't have notion of interface and implementation, so you would have to implement that as an object-like structure of function pointers and opaque structs for state.
The point is, for delegate you don't need lately bound methods and virtual dispatching, i.e. oop, only the notion of interface and implementation supported (or emulated), and then you can create implementations that wrap another implementations.
No, you don't. LocalRepo.t != Repo.t, and all the functions are purely static. LocalRepo.get_user won't work with other Repo.t, and other Repo.get_user won't work with LocalRepo.t. What you would need is to parametrize your further computation with a Repo module, as in
module HttpHandler (R : Repo) : sig
val handle_user : R.t -> HttpWriter.t -> username:string -> unit
end = struct
let handle_user repo writer ~username =
match R.get_user repo username with
| None -> Writer.write_status writer 404
| Some user -> Writer.write_body writer (User.to_string user)
end
and then to construct a handler module with a proper repo.
We forget that favoring composition over inheritance is one of the core ideas of OOP. It's supposed to be, at least.
Textbooks have an example like "Dog inherits from Animal" as a way to demonstrate the concept of inheritance, but people took that as an invitation to become Linnaeus and create a hierarchical taxonomy of their domain.
It might be a useful exercise, actually, but it's rarely the best way to design software and you can do OOP (and do it better) with sparing use of inheritance (by replacing it with extensive use of composition).
The #1 canary for me is circular dependencies. In more complex domains, a strict hierarchy of types begins to fall apart rapidly.
In my experience, a relational model is the best solution for domain modeling precisely because it can directly represent circular dependencies without any sort of weird interface hacks, one off Init() methods, etc.
A practical example of this would be something like the domain of banking. In a bank, you generally have 2 major types of entities: accounts and customers. Accounts can have many customers and customers can have many accounts. Which type should "win"? In SQL you don't have to make this decision at modeling time - it is deferred to the caller and its specific view of the world.
You can certainly emulate this in OOP, but then you are stuck doing joins between collections manually.
This is why I prefer multiple dispatch over classes: functions dispatch on the types of all arguments, not just the type of the implicit first argument. After first encountering this paradigm in Julia, it’s painful to use other languages that don’t have this feature.
I don’t think multiple dispatch solves this issue. Julia does have a strong type hierarchy and the proposed example with customers and accounts would be likewise hard to model. IMO only Rust like traits solves this issue elegantly.
I don't see the problem. You can have a class for accounts, a class for customers, and a class for the links between accounts and customers. Those classes are peers; there's no need for one to be the winner. Most OOP languages also have ORM frameworks that can help here. I've built stuff like that in Java and in practice it's just not issue.
Mainly hooks. Hooks make it easy to reuse component logic, something that was difficult with class components. You’d need HOC’s which are hard to deal with, especially with prop collisions and refs.
(This change has little to do with OOP vs functional. React calls them function components, not functional. React has always used functional patterns, even in the class components.)
Thanks I went and looked up the different approaches (have never written a line of React in my life) and I definitely like the hooks more on first blush.
React isn't actually functional. It's OOP in disguise. There's no such thing as functional UI, in the classical sense of FP. It's one of the weaknesses of FP.
Although React tutorials start by saying your UI becomes a pure function of your app's state, React components do have encapsulated state. Any time you call useState inside a react function you're doing something that doesn't exist in real FP programs. Additionally React boils down to a way to create DOM elements, which are highly stateful C++ objects exposed to JavaScript, implemented using inheritance:
An HTMLButtonElement is a HTMLFormControlElement is a HTMLElement (and a ListedElement and a FormAssociated).
The rules for how you write code in React (or Jetpack Compose) violate even the most basic rules of the host language. useState defines or returns what is effectively an object field (it's data associated with that widget), but it looks syntactically like a local variable. It's not a local variable though because its value will survive the exit of the scope. Nor is useState a normal function call because there's a hidden invocation counter used to work out what state slot you want, which is why you aren't allowed to call it inside conditional branches and why loops need special handling.
So in React's "functional style" we can see that you're actually defining objects with encapsulated state, that register event handlers, that export properties and so on. It's just obfuscated to look like it's not OOP.
I'm curious, do you have a current favorite way of implementing UI, particularly for desktop applications, that isn't obfuscated and doesn't violate the rules of the host language? Or do you find that Jetpack Compose is a good choice overall despite those shortcomings?
I'm still making up my mind about Compose, there are aspects I really like and aspects I really don't. It's not the fault of Compose itself, the aspects I don't like are basically the conceptual model as copied from React.
Honestly for recent simple GUIs I've made for my own in house tools I'm still using JavaFX. Mostly because I always seem to end up needing a good table view, and because I happen to know it. If I were working with it a lot I'd end up maintaining a TornadoFX style set of wrappers but for simple stuff the plain API works OK.
Because I'm weird I'd probably use it for low-traffic web apps these days too, with JPro (jpro.one). Probably not for mobile web, and not for stuff where users are on high latency connections in general, but if the user has a nice big screen and a low latency connection I'd personally find it a lot easier and more productive than dealing with HTML and CSS. The results are indistinguishable for the user if you are careful.
Thanks for that info. I promise the original intent of my question wasn't to find an excuse to nag about accessibility, but I should point out that JPro is certainly not indistinguishable from typical HTML for a screen reader user, judging by the JPro website. In particular, the links are marked up strangely, with a separate link for each word in a multi-word phrase, and there are no headings or landmarks to aid navigation. Just thought you should know.
you can certainly write functional UI using a differential 'pure core state on the outside model' (someone here had a discussion that gave this thing a name)
> Any time you call useState inside a react function you're doing something that doesn't exist in real FP programs.
Meh, it's just a monad, hidden in a fake syntax so people don't panic.
> Additionally React boils down to a way to create DOM elements, which are highly stateful C++ objects exposed to JavaScript, implemented using inheritance
And C++ and JavaScript are just ways to run machine code, which is fully unstructured. But all of that is an implementation detail. React creates DOM elements because that's what you have to do to render UI in the browser, not because its creators think the DOM is a good idea.
> So in React's "functional style" we can see that you're actually defining objects with encapsulated state, that register event handlers, that export properties and so on. It's just obfuscated to look like it's not OOP.
It's not OOP - it doesn't have inheritance, most state is (idiomatically) immutable and exposed, and as you noted, React state does not follow the same rules as OOP state. While it's not fully pure functional (I'd actually say the React state model is a novel and interesting thing, that I expect may end up being a language feature in the next generation of successors), it's closer to functional than traditional OOP, and most of the benefits are those of a functional rather than OO style.
Most state in React isn't exposed because it's managed by the OOP DOM. Scroll positions, focus, press state of buttons, cached bitmaps, autofill state, lists of event handlers etc.
Jetpack Compose now, that is a pure implementation of the React concept all the way to the bottom. And their compiler plugin basically makes the React model a part of the language, so you could go take a look at that if you're interested in language-integrated approaches. There's essentially no OOP there and all state is handled with their useState equivalent. But as I write in another post it actually does suffer badly from that decision, in my view. The combination of the React concept with static typing doesn't mix well at all. There's an astonishing amount of copy/paste coding going on in the Compose landscape and worse, a new generation are starting to learn that this is normal and just the way UI code looks.
On the web the React approach took off due to the history of JS/HTML not really supporting proper componentization or indeed proper OOP until very late (arguably it still doesn't). Then the Android guys decided to adopt it to be more familiar to web devs and as a way to justify the huge expense of starting over, which they did need, the old Android toolkit has a lot of tech debt in it.
But I think we will come to regret that evolutionary path. Once you get past the convenience of being able to add/remove widgets by rebuilding the whole layout from scratch, it's just not a good way to express UI in statically typed languages.
> Most state in React isn't exposed because it's managed by the OOP DOM. Scroll positions, focus, press state of buttons, cached bitmaps, autofill state, lists of event handlers etc.
For someone writing stuff in React all that is effectively the "runtime", and whether it's being managed in OOP, FP, or some other fashion is an implementation detail. (You don't e.g. subclass DOM classes when writing React).
> The combination of the React concept with static typing doesn't mix well at all.
Hasn't been my experience at all, shrug, my main job does a lot of stuff with React in Scala.js and it works very well.
React is not a UI library itself -- the actual UI stuff is delegated to the DOM. It is more like a library on top a UI library, that manages the state->representation flow (where reactive FP is indeed a good choice).
Yes, but the fact that react uses the DOM is because that is what the very restricted platform gives you out of the box, and opting for other options would have several disadvantages that are purely the result of those other options not being directly provided by the platform itself. A reactive UI library (like react) could very easily be implemented on top of other UI paradigms as well, including decidedly non-OOP ones. There's nothing special about object oriented UI systems that motivates reactive UI libraries to use them except for the fact that that's what the underlying platform provides because that was what was in fashion when the platform was taking shape.
That's also because they exist in a browser. The browser displays one page (from the viewpoint of a web app) and has one global event loop, the event loop calls into the application code. Application code doesn't run at other times. Reactive programming is a good fit for that.
> one global event loop, the event loop calls into the application code. Application code doesn't run at other times.
In the 80s we called that "event-driven programming", and until threads became widely available, it is how all native GUI programming worked, more or less.
Event-driven is the "reactive" part only. And the old implementations of it where so less capable of fine-handling the events and complex dependencies that it's reasonable to change the name.
I would disagree. Conventional desktop GUI toolkits offer much more fine-handling of events and complex event/data dependencies. Web front ends are now approaching those levels, though from a different direction.
I disagree on the UI side. UI has been moving towards more functional declarative approaches as it’s much easier to manage.
CSS is also interesting, we mostly try to avoid the inheritance with scoping, utility classes and or css in js solutions as it becomes really hard to manage otherwise.
Inheritance is likely the worst idea of OOP's implementations.
The right way to achieve the same and better results is interfaces / typeclasses / traits. They work pretty well in languages from Rust and Haskell to Typescript, Kotlin, Java, and even Go. (Go is a bit special because you don't even have to declare that your class implements an interface, as long as it actually has a set of methods with right signatures.)
Ideally, all public methods should only describe parameters and return values in terms of interfaces, and never as concrete classes.
If you need to take a big class and inherit from it to override one or two methods, it's a failure to compose the things correctly. These one or two methods usually are better off as their own entity which you can pass to the "base" as parameters. This both prevents proliferation of "god objects", increases testability, and improves build times (shallower dependency graph, yay).
> Ideally, all public methods should only describe parameters and return values in terms of interfaces, and never as concrete classes.
Citation needed. I've seen interface heavy Go code, and without fail it's always unreadable garbage. Interfaces are best used in moderation, at API boundaries only.
"Interfaces are best used in moderation" is not the same thing as "only use interfaces for public methods". the latter typically results in painful, unreadable code.
For me, personally, I’ve learned to use the right tool for the job.
In some cases, it’s OOP, in some cases, it might be something like FP, or Protocol-Oriented Design.
I write in Swift, which supports all of the above. It’s been a while, since I’ve worked with C++, but I have heard that the modern version of the language similarly supports multiple paradigms.
For the frontend, if I am writing for UIKit, I use OOP, and fairly standard MVP, as that is exactly what the framework was designed to afford. I haven’t done much SwiftUI, but I suspect that I’ll be using a lot less OOP, when I do.
On the server end, I tend to use good ol’ PHP. It’s not an especially good OOP language, but I find that OOP isn’t as relevant for my backend work. That work tends to look a lot like very old-fashioned Structured Programming, which PHP is good for. As others have pointed out, I’ve found it easy to go down rabbitholes, with backend OOP.
There’s a lot to be said for an approach as an experienced, multidisciplinary professional.
It seems as if programming and software engineering pundits generally start from the premise that all [other] software developers stink, and rules and tools should be used in an effort to force them to write “good code,” as opposed to, for example, hiring smart people, and training and mentoring them to be adaptable, resourceful, disciplined engineers.
That’s nothing new. I have been seeing this for my entire [long] career. The “new hotness” changes, but the attitudes are consistent.
OOP, FP and other paradigms are all useful. Just don't do extremes like everything is an object or everything is immutable or everything is without side effects.
Yep. Any given software project of non-trivial complexity is going to be composed of many types of problems, each of which requiring its own solution and approach. I don’t think I’ve ever worked on a codebase that didn’t need both OOP and FP in different places.
> Data abstraction
> Encapsulation
> Polymorphism
> Inheritance
Note that it's the last two concepts which are specific to object oriented programming. The first two are already supported by non OOP languages like C and enable implementation of abstract data types. I think it's unfortunate that all concepts are often introduced in the same OOP course which may make the students believe that a class and an abstract data type is the same thing.
> Encapsulation as an idea failed spectacular again and again. What would have been valuable would have been a way to enforce layers of abstractions, with no composition or inheritance of objects on the same layer.
Polymorphism is not at all specific to OOP, and there are in fact different kinds of polymorphism; "generics" are pervasive in pure FP languages such as Haskell or imperative/functional languages like Rust, and are an instance of parametric polymorphism. Ad-hoc polymorphism, or "operator overloading," can also be seen in both languages as the "Eq" typeclass/trait and the equality operator `==`. It's subtype polymorphism and deeply nested inheritance structures that are maybe particular to OOP.
It's also worth making a distinction between an abstract data type - a "thing" and some set of operations defined over it - and a data structure - a particular in-memory construction and set of procedures defined over it, often used to realize an abstract data type. An example being a dictionary ADT - supporting get, set, delete, etc. being realizable through different data structures; for example, a linked "association list" of (key, value) pairs vs. a hash table - with each having different performance characteristics, despite possessing the same properties as the ADT they implement.
Polymorphism that is resolved at run-time, i.e. "virtual" functions is what is specific to OOP.
The term "polymorphism" was first used in the documents of the language CPL (in mid sixties), the source of BCPL, where in British English it corresponded to what in the American documents of the same age, starting with McCarthy, were called overloaded operators and overloaded functions.
The use of "polymorphism" mostly for the run-time polymorphism of OOP languages has appeared only a couple of decades later, with Smalltalk.
"Generic", which might be a better term than both, has been popularized during the programming language contest that has lead to Ada (1979). Nowadays it is mostly used for polymorphism/overloading that is resolved at compile time, because this is how the term has been used in Ada.
Even polymorphism that is resolved at run-time is not specific to OOP. In fact, it's used quite extensively outside of OOP, including in both functional and procedural programming. Even ignoring the types of dynamic polymorphism present in other paradigms that might be called "OOP-inspired", there are commonplace patterns such as the use of closures that let you achieve run-time polymorphism and data encapsulation.
What you call an abstract data structure is what I have learned is called a concrete data type. To my understanding an abstract data structure is a module with global (single instance) private data which is manipulated using the module's interface. In OOP terminology it's a singleton class.
I mean, you can do OO type stuff with anonymous handles but the compiler will let a function take one handle type where another belongs and things will fail opaquely but immediately if you're luck and fail slowly if you're unlucky. Which is to say there is no "support".
You simply declare a type as a pointer to a struct in the header file along with the functions that operate on variables of this type, and define the struct and the functions in the implementation file.
That’s not encapsulation, that’s just an ugly hack.
Somewhat related, it has been shown that a language with public/private visibility modifiers is more expressive than one without (that is, it can’t be locally replaced by another expression that results in the same program).
> That’s not encapsulation, that’s just an ugly hack.
Call it what you want but it works well. Declaring a variable or function as static corresponds to "private". I honestly don't understand what you mean by "locally replaced by another expression", I think you'll need to exemplify.
The slide deck is about performance pitfalls of OOP languages, which can surreptitiously introduce cache misses, random access to memory, and excessive levels of indirection into your code:
> Prefetching
> Data accesses are now predictable
I'm reminded of a Stroustrup talk in which he says that vectors will always outperform linked list structures, by taking advantage of spatial locality and predictable access patterns to enable better cache prefetching. He even has a slide on C++ vs "True OO" style object graphs, and it looks remarkably similar to what's described in the OP: https://www.youtube.com/watch?v=YQs6IC-vgmo&t=3m53s
That went in a different direction than I expected it to - but it's also not specific to object oriented programming. Richard Stevens summarized this 40 years ago: "modularity is the enemy of performance". We've mostly accepted that this is an acceptable tradeoff, although the author (and I) aren't fully on board.
When the whole OOP movement gained traction I struggled to accept the degree of value it provided above simple structs and functions (at least for an organized programmer).
After decades developing on a lot of diverse platforms in a variety of languages and models, I've decided there ain't much you can't achieve without structs and functions (or their equivalent).
Sure, any tool is most efficient in the hands of a master who knows how to wield it. But in practice all those shiny extras too often become productivity distractions.
To me, the big difference between a struct and an object is when data has constraints.
Let's say I have a rotation matrix. That means that the squares of all the entries in any one row or column add up to 1. If I have such a structure, and I find that it has rows that no longer add up to 1, well, what function messed it up? I have no idea. And if I have a million lines of code, that gives me a lot of places I have to look.
But with an object, if I made that data private, only a member function can have messed up. I may still have a thousand lines I have to look at, but that's a lot less than a million. For larger code (tens of thousands of lines and larger), I consider access control to be essential, just to limit the size of the code that could possibly be at fault.
Back in the day, I spent a lot of time playing "who messed up the structure?" in structure-and-function code...
Congratulations, you have discovered Turing Completeness.
But it is not a contest of what can be done, but rather of how clearly, succinctly, and performantly it can be done. Modularity is essential to human understanding, but inimical to performance. Succinctness demonstrates economy of expression, where it matters that what people need done is more succinct than things they don't.
OO as a movement was childishly silly. OO as an organizational technique among many has been fruitful.
Anything-oriented programming amounts to tying a hand and foot behind your back. Any big enough program can benefit from OO in certain spots, functional in others, relational here and there, generic throughout all the others, pipelined, dataflow, declarative, what-have-you. The problem dictates its solution.
129 comments
[ 3.6 ms ] story [ 212 ms ] thread> • What is OO programming? – a programming paradigm that uses "objects" – data structures consisting of datafields and methods together with their interactions – to design applications and computer programs. (Wikipedia)
It isn't that they are together, it's that the core entity of the language used to express solutions centers around identity over state or values. As in: two objects are different even when the fields are all equal, and also: two objects can be the same even with different field values.
If you are going to object to this, I ask that you first contemplate whether or not you have programmed in any other way first, because as a fish, reasoning about what water is can be difficult.
so i feel secure in claiming that i am more a walrus than a fish and therefore qualified to opine about water
i do agree that identity and hidden mutability are important aspects of the oo view of the world, and that the author of these slides is off-base about what oo is. but i think that the essence of oo is that computation is done by sending messages to objects, but sending the same message to different objects invokes different code. this feature is present without mutability in bicicleta, and in abadi and cardelli's untyped ς-calculus, which it's based on; the ς-calculus was invented to formalize the essence of object-oriented typing
the original intuition behind oo was to decompose a computer system into smaller computer systems, like a computer network; each object in the system was in effect a tiny computer, with its own private code and state. this was inspired by alan kay's undergraduate degree in biology, which mostly studies organisms made out of cells, which are basically tiny organisms. and in that formulation, obviously hidden mutability is the default, because you can't prevent a computer from changing its internal state as a result of a packet you send it; and mutability generally means identity is important
but even more central to this formulation is the idea that different objects can behave differently in the same circumstances
obviously none of this has anything to do with memory layout! except very indirectly
If you boil it down to the essence, this means branching logic is encoded via a function pointer. Instead of manually switching to the correct behavior using other branching mechanisms like if or switch.
This is what polymorphism is about. It's pattern matching.
An interface is like an Enum to which you can add entries after it has been defined, and the behaviour for that specific variant naturally then has to be encoded inside the variant itself.
And thus, "more natural" than, let's say, procedural programming.
Animals can be modeled nicely with objects/classes. Physical objects do have internal private state and public interfaces. Also in nature, usually, you have multiple similar objects that work the same but have different internal state.
But even if you're not inspired by biology, it might just come up "naturally". Separating your data into structs, then operations on those structs, and not wanting to deal with all kinds of bookkeeping all the time, letting things manage their own - separation of concerns, aka single responsibility principle.
I think automatic memory management might be a big example of this. Who wants to bother with this malloc/free stuff all the time? Who wants to keep track of ownership everywhere? Instead, have a dedicated subsystem handle it. That allows a higher signal to noise ratio in all the rest of the code that would otherwise be polluted with malloc/free. It also removes the entire class of bugs that can happen when you inevitably forget either of those.
But having memory management in and of itself isn't "OOP". It's just an example of one of the principles that people tout when they talk of "OOP". Those principles can and do apply broadly.
Was Alan Kay thinking of all this? I really don't know. All I know is that the dude digs biology.
https://lists.squeakfoundation.org/pipermail/squeak-dev/1998...
It's a concept I really like and use quite a bit in game development at the inter/intra-entity communication level even if below that I'd pick a more performant substrate.
https://archive.org/details/byte-magazine-1981-08/page/n75/m...
"Design Principles Behind Smalltalk" Byte Magazine 1981
https://archive.org/details/byte-magazine-1981-08/page/n299/...
This doesn't seem like a sufficient definition, as closures are also sub programs with their own state and behaviors that implement a common interface (the function signature), and you'd have to stretch the definition of OOP to the point of meaninglessness if that would be sufficient to qualify as OOP.
[0]: https://handbook.selflanguage.org/2017.1/progguid.html#behav...
It's that the receiver of those messages have an identity that defines it.
Inheritance and polymorphim is of course central to OO, but it's not defining characteristics imho because inheritance is "module arithmetic" and polymorphism has plenty of flavours.
(I am in no way trying to have a go at OO either.)
The defining feature of OO approaches is in shifting the emphasis in sw development into the concept of identity-bags. All the other stuff about message passing, encapsulation, inheritance etc is just window dressing and implementation details of this core concept of identity-classification in software systems.
I like how the "Out Of the Tarpit" (2006) paper breaks this down into the notion of "intentional" vs "extensional" identity, and I think this is a great model.
For them, OO is all about "intentional identity" in that we create identities (objects, components) in our systems and move attributes and behavior onto them. OO shifts the analysis of software to identifying objects, components, services, and then makes software development all about the taxonomical management of them. In this model of software development, it's all about identifying components and then shuffling the data and functionality "into" them and managing the flow between them. (FWIW the paper identifies this way of thinking as one of the prime culprits in unnecessary complexity in software systems, and I agree with them.)
The contrast is "extensional" identity, where identity in the software systems are emergent from their attributes. In this box we can put the relational data model, and its cousins in logic programming (prolog, datalog etc). In those systems, we are not concerned with "creating" identities and classifying systems/components/objects, but instead declaring the raw attributes -- tuples, facts, relations -- only. Queries then produce, at runtime, a multitude of results from permutations of those attributes. And, as you point out, equality/inequality is defined by the value of a tuples attributes, not by the object it lies in. This is crucial.
There's a couple generations of software developers that are so immersed in the OO/intentional-identity way of thinking that they don't see that it's not intrinsic to software development, but actually a mental framework that was fostered through the 80s and 90s. It was an attempt to manage complexity and structure in software systems and also to create a marketplace of re-usable components. But I think it's actually failed at that.
Most classes, objects, components, microservices end up failing in two respects: they end up reflecting the company's org chart more than anything else, and .. worse .. the bulk of the code and complexity ends up being how one moves data between all these (programmer created) systems rather than what the actual data is. The relationships (lines) between the boxes becomes the focus instead of the stuff was shoved into the box, and the whole behavior of the system becomes difficult to reason about.
And when developers encounter systems that don't work along this model -- relational databases, functional programming, logic programming -- they often scratch their heads, resist, or wrap them up/confine them in OO/component type structures... damaging their usefulness.
With that in mind, I do believe that objects, as in encapsulating some kind of side-effect, is a very good model. But it is not a replacement for everything - a more data-oriented approach makes much more sense for some other parts of a program. Multiple views are absolutely possible within the same language/program, and should in general be the preferred way.
So a "class" (and object identity) is really telling the system: when you see attributes X, Y, Z bundled together as a result of a query, here is a set of behaviors that you can apply on those tuples.
Like, I think we should think of a `Connection` as an object, not `User`, which is just plain data.
Maybe structural typing is somewhat similar to what you think of? Probably typescript, maybe go are the most well-known for this out of the statically typed ones, but clojure is also all about every “object” being a key-value map.
And I don't think it's so much structural typing, as it is defining method/function dispatch off predicate matches. More.. structural values? Clumsily:
Disconnect(connection_id) = with project ConnectionState CS where CS.Id = connection_id { if CS.state != 'connected Raise('not_connected) else CS.state = 'disconnected }
I've come to appreciate these difference in later efforts to learn functional programming as a way of giving myself more tools. But for the longest time, it was very difficult for me to "get the boxes right" for what things go where. What is a module, an object, a function, a method, etc, coming from the object oriented universe.
I still have the urge to "create objects" when I conceptualise solutions and _imho_ this isn't always a good path.
These days OO exists within my horizon of tools for solving tasks. Before, it was the water, if you will. Some problems have a want for objects, others don't really.
Sadly he suicided in 2011 with Sodium Pentothal he bought mail order from China. My final Skype message to him was "goodnight, don't drink the pentothal!" and Skype used to show it to me under his username every time I logged in.
He, and his acerbic nature, are sorely missed.
[0] https://www.reddit.com/user/uriel/
1. https://www.youtube.com/watch?v=VAT9E-M-PoE
2. https://docs.google.com/presentation/d/1ST3mZgxmxqlpCFkdDhtg...
3. https://github.com/dbartolini/data-oriented-design
What OO has become in the industry is a product of practical concerns encountered by people working in the field. Can't say I like it much, but working in this industry for 25ish years, I can understand the pragmatic "whys" of every step of language and tool design fairly well.
(In a way, even CSS is kind of OOP, albeit with very confusing overriding rules.)
When writing generic backend code in a language like Java, I often found fighting against OOP because the business logic is not necessarily mappable to "dog inherits from animal". And I can't change the business logic, so OOP has to give in. But when developing user interfaces, I am usually in more control, I am actually designing it, not observing directly from the business logic. And OOP forces me to have a more consistent implementation, which is beneficial for the user, because consistent UI makes it easy for people to get used to it, find the underlying logic in it, and start relying on it. If all links on all web pages would inherit from normal HTML href link (like it was designed in the beginning), we wouldn't suffer from those weird JS web apps where you can't open a link in a new page for example.
> there could be an element inheriting low-level behavior (HTTP/API call, etc) from a button and a high-level behavior (style/presentation, etc) from a visual class.
This becomes very messy very quickly, and you would need to clearly separate the concerns to make them non-expressible on either side. CSS and JS make this possible by being two different languages, one Turing-incomplete (in usual scenarios).
You avoid duplicated code by making generic functions, putting them in modules, and making your objects accept them as parameters.
Regardless, the inheritance remains an implementation detail.
[1]: https://docwiki.embarcadero.com/RADStudio/Alexandria/en/Inte...
I admit I would really like a 'delegate function call to <object>' construct, but excuse me for being dense, what composition are you referring to? Other than inheritance and delegation, what else is there[1] , and what would a suitable construct supporting it in the language look like? TIA
[1] (there is functional-style composition, but that's perfectly well supported in many decent languages like Scala)
Yeah, I'd love to see Kotlin's delegation[0] developed further and appear in other languages as well.
[0]: I admit I would really like a 'delegate function call to <object>' construct
One reason is that inheritance is strictly more powerful. When you delegate (compose), you can't modify the inner working of the object you delegated to, you can only wrap it. Often that's not sufficient. You want to actually customize the implementation in some way. OOP's inheritance is ideal for this, because the superclass can clearly define chunks of functionality that subclasses are allowed to either (a) replace entirely or (b) wrap or (c) provide (abstract methods).
This is a very flexible approach. With mere delegation you can only wrap, and only for method calls that originate outside the object, and only for public methods.
It's also clean. The distinction between what you must implement, what you can implement and what is for the user is defined in the type system and enforced by the compiler. Additionally you separate the interface for customizations from the interface for users (protected vs public), which keeps documentation and auto complete properly focused.
When combined with dependency injection and code generation it also allows third party frameworks and libraries to enhance the code you write in various ways, that remain mostly tucked out of the way but can be revealed in an IDE in an instant (and a good IDE will show visual hints that there are such customizations in effect).
The final advantage is that in some languages the implementation is highly optimized. On HN you'd be forgiven for thinking nobody writes virtual method calls since 20 years ago but in reality virtual method dispatch is so common in real programs that it's heavily optimized by both CPUs (sophisticated indirect branch caching) and language runtimes (PICs in fast VMs like Hotspot or V8). Composition is less well optimized.
Inheritance gets an overly bad rap on Hacker News, despite how prevalent it is. I think this is because it's so flexible you can easily make a mess with it, and some people have. Also the decision of Java to make all classes and methods open by default means overriding is often used as a hacky way to hot-patch libraries in the field, rather than as part of any principled overall design. Kotlin reverses that decision to some extent and so classes and methods are all final by default, unless a compiler plugin overrides that.
I also suspect the move to web development has had this effect, because in most classical OOP toolkits you are allowed or even encouraged to extend the UI by subclassing pre-existing node types. So you can make a custom button by subclassing Button or whatever. HTML is implemented using classical OOP designs, but mostly for implementation reasons that isn't exposed to the web developer who instead is expected to customize the controls purely through CSS and setting properties. If you want to make a custom button widget, well that's too bad, better learn React or something similar. So people just aren't exposed to the situations where OOP is useful because browsers weren't designed for UI, and the memory of why it was originally developed atrophies.
And some of it is just memeing.
OK, now tell me why I'm wrong.
> Often [delegation] not sufficient. You want to actually customize the implementation in some way. OOP's inheritance is ideal for this
The usual way of customising behaviour without inheritance is to pass in an object to actually implement the behaviour.
vs Which customises the logger class. Trivial example but I use this a lot.> I think this is because [inheritance is] so flexible you can easily make a mess with it, and some people have
agreed
C++/Java/Kotlin style OOP is basically about putting common design patterns into language syntax. You could do it all with C, but it's easier if the compiler does it for you. For example Kotlin has "data classes" and Java now has "records" which are in both cases classes, but they sacrifice inheritance in order to auto-generate more boilerplate for you. That's still OOP though.
It's more powerful, sure, but in the same way that goto is more powerful than if/for/while. In theory some languages give you the tools for a superclass to expose a stable interface to its subclasses (ish - even Java doesn't give you a way to specify "subclasses may read but not write this field", for example), but in practice they're rarely used (Wicket is literally the only library I've seen to offer an effective inheritance interface); the overwhelming majority of the time inheritance ends in fragile base classes and the resulting bugs.
Which is perhaps inevitable; the trouble with giving you the tools to change the superclass's behaviour in unintended ways is that it results in people changing the superclass's behaviour in unintended ways. If you have a locked-down inheritance model where classes and methods are final by default then maybe you avoid the fragility problem - but by the same token if subclasses can only use extension points that were deliberately exposed then you're probably not gaining much flexibility over what you could do with composition.
> The final advantage is that in some languages the implementation is highly optimized. On HN you'd be forgiven for thinking nobody writes virtual method calls since 20 years ago but in reality virtual method dispatch is so common in real programs that it's heavily optimized by both CPUs (sophisticated indirect branch caching) and language runtimes (PICs in fast VMs like Hotspot or V8). Composition is less well optimized.
Shrug, as the language design consensus shifts, the implementation optimizations will follow. Composition should ultimately be easier to get better performance out of, because it's more constrained (in particular, knowing that a given class or method is final means that you can optimise that class or method much more aggressively).
> So people just aren't exposed to the situations where OOP is useful because browsers weren't designed for UI, and the memory of why it was originally developed atrophies.
AIUI the association between OOP and GUI is more of a historical coincidence than a deliberate design. Though I agree that they do seem to fit together well.
I think I know what you mean, but you can easily define a private field with a protected get method which amounts to the same thing.
In Kotlin there's no distinction between properties and fields, so you can write:
to express that.> in practice they're rarely used
I'm not sure I agree. Most Java libraries use it, and it seems to work fine. Mobile UI has used it for many years and that's an absolute ton of code. Game engines use it, games are a huge chunk of the software industry. People would use it a lot more if the web worked differently or if we had better non-web distribution systems on the desktop.
> if subclasses can only use extension points that were deliberately exposed then you're probably not gaining much flexibility over what you could do with composition.
You can certainly do everything that inheritance allows without support for it in the language, but that doesn't say much. You can also do OOP in C, people do, but it's generally more convenient to have compiler support. Used properly, inheritance can certainly lead to much more convenient APIs and code without being prone to misuse.
As an example, one of the issues I have with Jetpack Compose is that if you want to customize the controls in any way you have to wrap them. There's no way to say "I want a MyButton that's the same as Button except in this one aspect", because OOP is gone. You have to define a totally new MyButton function/composable that delegates to Button. In JS it's just about tolerable due to the dynamic typing and related features (e.g. spread operator), but in Kotlin it doesn't work despite relying on compiler plugins that change the language's core semantics. You end up with huge screenfuls of boilerplate which are just forwarding function arguments from one place to another, and if you upgrade to a new Compose do you get the new features automatically? No! Due to its browser heritage React conflates widget libraries with theming, so you've had to wrap all the widgets to get anything beyond the basic Material Design look, and that's manually generated boilerplate so you'll have to go and plumb new properties through by hand. There's also no nice way for the authors of Button to define properties that are only intended for customizers vs users because there's no concrete notion of customization at all. Ugh.
Now compare to an OOP toolkit:
You upgrade the base libraries and MyButton now has the new features automatically, no code changes required.> knowing that a given class or method is final means that you can optimise that class or method much more aggressively
Virtual method calls are treated as final when possible in good language VMs anyway, so it makes no difference. They're only emitted as virtual when there are actually multiple receivers observed at that specific call site, which would have to be implemented as a switch{} in composition, so again, you don't gain anything but do lose language convenience.
It's not though, a method is significantly more cumbersome. People get lazy and make it a protected field, and probably they themselves remember that the subclass mustn't change it and it's only the poor next person taking over maintenance who gets bitten.
> Most Java libraries use it, and it seems to work fine.
I mean specifically offering a restricted interface for inheritance by making extensive use of "final", making unstable fields and methods inaccessible etc.. In my experience libraries are rarely careful enough about that, and upgrading a library that you've subclassed from is usually a fraught process. (Wicket is literally the only such library that I've ever been able to fearlessly upgrade)
> Used properly, inheritance can certainly lead to much more convenient APIs and code without being prone to misuse.
I'm genuinely not convinced. I think if you have interfaces and delegation support, then a well designed non-inheritance API (which might involve e.g. splitting what were single classes in the inheritance version into two) can be just as convenient.
> You end up with huge screenfuls of boilerplate which are just forwarding function arguments from one place to another
Isn't the whole point of delegation as a language feature to avoid that? Can't you just say "delegating this interface to this member" and then all the methods you didn't specifically define get forwarded?
> You upgrade the base libraries and MyButton now has the new features automatically, no code changes required.
Or it breaks, because the base library changed in a way you didn't expect / didn't take into account the way you've subclassed it.
that grossly exaggerates it. It's annoying but nothing like that.
Yes but Kotlin delegation is implemented for objects and interfaces, not functions. Compose is React inspired so it uses functions with keyword arguments for everything, so if a new keyword argument is used, you have to manually forward it.
> Or it breaks
It can happen for any kind of API upgrade, especially where you can customize large amounts of complex logic. I don't think it's unusually worse for inheritance based designs. Obviously that's predicated on there actually being a design, but that's not a problem unique to Java (e.g. C libs that export non-stable symbols is a perennial issue).
Because OOP is somewhat equals to inheritance (or more strictly late binding + open recursion + self-polymorphism, inheritance is just one way to achieve that).
"delegating" is completely orthogonal to OOP, you can have delegates in any modular language, like ML. Saying "you need to use more delegates and less inheritance" basically equals to "you need to write less OOP code".
I'm struggling to understand this, could you give an example?
The point is, for delegate you don't need lately bound methods and virtual dispatching, i.e. oop, only the notion of interface and implementation supported (or emulated), and then you can create implementations that wrap another implementations.
That's because inheritance isn't really elsewhere, making it uniquely an OO concept (though I'm sure there are exceptions).
When people say OO is all about X, Y, and inheritance, well, X and Y are in most of the other languages too.
That's what OO is to me. Inheritance is nice for some things, but entirely optional.
Textbooks have an example like "Dog inherits from Animal" as a way to demonstrate the concept of inheritance, but people took that as an invitation to become Linnaeus and create a hierarchical taxonomy of their domain.
It might be a useful exercise, actually, but it's rarely the best way to design software and you can do OOP (and do it better) with sparing use of inheritance (by replacing it with extensive use of composition).
In my experience, a relational model is the best solution for domain modeling precisely because it can directly represent circular dependencies without any sort of weird interface hacks, one off Init() methods, etc.
A practical example of this would be something like the domain of banking. In a bank, you generally have 2 major types of entities: accounts and customers. Accounts can have many customers and customers can have many accounts. Which type should "win"? In SQL you don't have to make this decision at modeling time - it is deferred to the caller and its specific view of the world.
You can certainly emulate this in OOP, but then you are stuck doing joins between collections manually.
What about cycles?
For example, "List all distinct customers from all accounts that are owned by customers who are members of some other subset of accounts".
I tried it with ChatGPT and it did produce a robust solution. This does not seem to be a limitation for strong type hierarchy after all:
```
# Define a struct for Customer struct Customer
end# Define a struct for Account
struct Account
end# Define a struct to manage the many-to-many relationship
struct CustomerAccount
end# Define a struct for the Bank which holds all information
struct Bank
end```
Indeed. It produced a relational solution that emulates what you'd do with SQL tables.
This is approximately the schema shape we use in our product today.
("We recommend defining components as functions instead of classes." at top of page at https://react.dev/reference/react/Component)
(This change has little to do with OOP vs functional. React calls them function components, not functional. React has always used functional patterns, even in the class components.)
Although React tutorials start by saying your UI becomes a pure function of your app's state, React components do have encapsulated state. Any time you call useState inside a react function you're doing something that doesn't exist in real FP programs. Additionally React boils down to a way to create DOM elements, which are highly stateful C++ objects exposed to JavaScript, implemented using inheritance:
https://source.chromium.org/chromium/chromium/src/+/main:thi...
An HTMLButtonElement is a HTMLFormControlElement is a HTMLElement (and a ListedElement and a FormAssociated).
The rules for how you write code in React (or Jetpack Compose) violate even the most basic rules of the host language. useState defines or returns what is effectively an object field (it's data associated with that widget), but it looks syntactically like a local variable. It's not a local variable though because its value will survive the exit of the scope. Nor is useState a normal function call because there's a hidden invocation counter used to work out what state slot you want, which is why you aren't allowed to call it inside conditional branches and why loops need special handling.
So in React's "functional style" we can see that you're actually defining objects with encapsulated state, that register event handlers, that export properties and so on. It's just obfuscated to look like it's not OOP.
Honestly for recent simple GUIs I've made for my own in house tools I'm still using JavaFX. Mostly because I always seem to end up needing a good table view, and because I happen to know it. If I were working with it a lot I'd end up maintaining a TornadoFX style set of wrappers but for simple stuff the plain API works OK.
Because I'm weird I'd probably use it for low-traffic web apps these days too, with JPro (jpro.one). Probably not for mobile web, and not for stuff where users are on high latency connections in general, but if the user has a nice big screen and a low latency connection I'd personally find it a lot easier and more productive than dealing with HTML and CSS. The results are indistinguishable for the user if you are careful.
Meh, it's just a monad, hidden in a fake syntax so people don't panic.
> Additionally React boils down to a way to create DOM elements, which are highly stateful C++ objects exposed to JavaScript, implemented using inheritance
And C++ and JavaScript are just ways to run machine code, which is fully unstructured. But all of that is an implementation detail. React creates DOM elements because that's what you have to do to render UI in the browser, not because its creators think the DOM is a good idea.
> So in React's "functional style" we can see that you're actually defining objects with encapsulated state, that register event handlers, that export properties and so on. It's just obfuscated to look like it's not OOP.
It's not OOP - it doesn't have inheritance, most state is (idiomatically) immutable and exposed, and as you noted, React state does not follow the same rules as OOP state. While it's not fully pure functional (I'd actually say the React state model is a novel and interesting thing, that I expect may end up being a language feature in the next generation of successors), it's closer to functional than traditional OOP, and most of the benefits are those of a functional rather than OO style.
Jetpack Compose now, that is a pure implementation of the React concept all the way to the bottom. And their compiler plugin basically makes the React model a part of the language, so you could go take a look at that if you're interested in language-integrated approaches. There's essentially no OOP there and all state is handled with their useState equivalent. But as I write in another post it actually does suffer badly from that decision, in my view. The combination of the React concept with static typing doesn't mix well at all. There's an astonishing amount of copy/paste coding going on in the Compose landscape and worse, a new generation are starting to learn that this is normal and just the way UI code looks.
On the web the React approach took off due to the history of JS/HTML not really supporting proper componentization or indeed proper OOP until very late (arguably it still doesn't). Then the Android guys decided to adopt it to be more familiar to web devs and as a way to justify the huge expense of starting over, which they did need, the old Android toolkit has a lot of tech debt in it.
But I think we will come to regret that evolutionary path. Once you get past the convenience of being able to add/remove widgets by rebuilding the whole layout from scratch, it's just not a good way to express UI in statically typed languages.
For someone writing stuff in React all that is effectively the "runtime", and whether it's being managed in OOP, FP, or some other fashion is an implementation detail. (You don't e.g. subclass DOM classes when writing React).
> The combination of the React concept with static typing doesn't mix well at all.
Hasn't been my experience at all, shrug, my main job does a lot of stuff with React in Scala.js and it works very well.
Though most contemporary frameworks (react, elm, android compose) tend to prefer (functional) reactive programming.
In the 80s we called that "event-driven programming", and until threads became widely available, it is how all native GUI programming worked, more or less.
CSS is also interesting, we mostly try to avoid the inheritance with scoping, utility classes and or css in js solutions as it becomes really hard to manage otherwise.
Inheritance makes for a very powerless form of logical programing. CSS is way more than OOP.
(Powerless is always preferred if it's sufficient, but CSS clearly shows a single hierarchy isn't sufficient.)
The right way to achieve the same and better results is interfaces / typeclasses / traits. They work pretty well in languages from Rust and Haskell to Typescript, Kotlin, Java, and even Go. (Go is a bit special because you don't even have to declare that your class implements an interface, as long as it actually has a set of methods with right signatures.)
Ideally, all public methods should only describe parameters and return values in terms of interfaces, and never as concrete classes.
If you need to take a big class and inherit from it to override one or two methods, it's a failure to compose the things correctly. These one or two methods usually are better off as their own entity which you can pass to the "base" as parameters. This both prevents proliferation of "god objects", increases testability, and improves build times (shallower dependency graph, yay).
Citation needed. I've seen interface heavy Go code, and without fail it's always unreadable garbage. Interfaces are best used in moderation, at API boundaries only.
Well, yes, at API boundaries, as you say, or in public methods, as I say. We agree!
In some cases, it’s OOP, in some cases, it might be something like FP, or Protocol-Oriented Design.
I write in Swift, which supports all of the above. It’s been a while, since I’ve worked with C++, but I have heard that the modern version of the language similarly supports multiple paradigms.
For the frontend, if I am writing for UIKit, I use OOP, and fairly standard MVP, as that is exactly what the framework was designed to afford. I haven’t done much SwiftUI, but I suspect that I’ll be using a lot less OOP, when I do.
On the server end, I tend to use good ol’ PHP. It’s not an especially good OOP language, but I find that OOP isn’t as relevant for my backend work. That work tends to look a lot like very old-fashioned Structured Programming, which PHP is good for. As others have pointed out, I’ve found it easy to go down rabbitholes, with backend OOP.
There’s a lot to be said for an approach as an experienced, multidisciplinary professional.
It seems as if programming and software engineering pundits generally start from the premise that all [other] software developers stink, and rules and tools should be used in an effort to force them to write “good code,” as opposed to, for example, hiring smart people, and training and mentoring them to be adaptable, resourceful, disciplined engineers.
That’s nothing new. I have been seeing this for my entire [long] career. The “new hotness” changes, but the attitudes are consistent.
Here’s an old joke, from the 1980s: https://news.ycombinator.com/item?id=29345907
With PHP, I tend to use OOP constructs as namespaces.
My backend work is fairly humble. Most folks here, would laugh at it, but it works really, really well.
I enjoy multi-paradigm languages the most.
It's also worth making a distinction between an abstract data type - a "thing" and some set of operations defined over it - and a data structure - a particular in-memory construction and set of procedures defined over it, often used to realize an abstract data type. An example being a dictionary ADT - supporting get, set, delete, etc. being realizable through different data structures; for example, a linked "association list" of (key, value) pairs vs. a hash table - with each having different performance characteristics, despite possessing the same properties as the ADT they implement.
The term "polymorphism" was first used in the documents of the language CPL (in mid sixties), the source of BCPL, where in British English it corresponded to what in the American documents of the same age, starting with McCarthy, were called overloaded operators and overloaded functions.
The use of "polymorphism" mostly for the run-time polymorphism of OOP languages has appeared only a couple of decades later, with Smalltalk.
"Generic", which might be a better term than both, has been popularized during the programming language contest that has lead to Ada (1979). Nowadays it is mostly used for polymorphism/overloading that is resolved at compile time, because this is how the term has been used in Ada.
I mean, you can do OO type stuff with anonymous handles but the compiler will let a function take one handle type where another belongs and things will fail opaquely but immediately if you're luck and fail slowly if you're unlucky. Which is to say there is no "support".
You simply declare a type as a pointer to a struct in the header file along with the functions that operate on variables of this type, and define the struct and the functions in the implementation file.
M.h
M.cSomewhat related, it has been shown that a language with public/private visibility modifiers is more expressive than one without (that is, it can’t be locally replaced by another expression that results in the same program).
Call it what you want but it works well. Declaring a variable or function as static corresponds to "private". I honestly don't understand what you mean by "locally replaced by another expression", I think you'll need to exemplify.
> Prefetching
> Data accesses are now predictable
I'm reminded of a Stroustrup talk in which he says that vectors will always outperform linked list structures, by taking advantage of spatial locality and predictable access patterns to enable better cache prefetching. He even has a slide on C++ vs "True OO" style object graphs, and it looks remarkably similar to what's described in the OP: https://www.youtube.com/watch?v=YQs6IC-vgmo&t=3m53s
After decades developing on a lot of diverse platforms in a variety of languages and models, I've decided there ain't much you can't achieve without structs and functions (or their equivalent).
Sure, any tool is most efficient in the hands of a master who knows how to wield it. But in practice all those shiny extras too often become productivity distractions.
Let's say I have a rotation matrix. That means that the squares of all the entries in any one row or column add up to 1. If I have such a structure, and I find that it has rows that no longer add up to 1, well, what function messed it up? I have no idea. And if I have a million lines of code, that gives me a lot of places I have to look.
But with an object, if I made that data private, only a member function can have messed up. I may still have a thousand lines I have to look at, but that's a lot less than a million. For larger code (tens of thousands of lines and larger), I consider access control to be essential, just to limit the size of the code that could possibly be at fault.
Back in the day, I spent a lot of time playing "who messed up the structure?" in structure-and-function code...
Being able to make a field of a record private does have nothing to do with OOP, even in C you can hide a struct's fields.
But it is not a contest of what can be done, but rather of how clearly, succinctly, and performantly it can be done. Modularity is essential to human understanding, but inimical to performance. Succinctness demonstrates economy of expression, where it matters that what people need done is more succinct than things they don't.
OO as a movement was childishly silly. OO as an organizational technique among many has been fruitful.
"Specialization is for insects."