Does OO really match the way we think (1997) [pdf]
leshatton.org
leshatton.org
Real world objects are a composition of parts. The taxonomy of those objects, and its constituent parts, is am artificial construct independent of how those objects are composed. A frog is a frog because it fits some definition that experts agree on. The taxonomy comes afterwards - the composition just is.
What we have is a model where the taxonomy comes first. We say a frog inherits from an amphibian. What makes an amphibian an amphibian doesn't matter in OOP. Further, the language rarely provides the same means for composition as it does for inheritance and taxonomy. That's a problem.
In fact, the only reason why a frog is an amphibian is because it has parts all amphibians share. The label comes afterwards, after looking at the constituent parts.
In this way, OOP would benefit from composition-first, taxonomy after. An object is a thing because of what it has (and this includes behaviour too), not because of what it is or descended from.
It should be entirely possible for me to construct an object that fits the definition of a frog without specifying I'm creating a frog.
The problem with this approach is that different domain experts will have different definitions of what "is" a frog. If you ask a theologist, a molecular biologist, and a zoologist to define a frog, you'll certainly get three different but valid answers.
My point is supported by what you say, in fairness. A taxonomy is artificial and separate to the actual object itself. It can and is different based on who defines the taxonomy. My argument is that this is why OOP suffers: it makes the taxonomy inherent to the object and all best OO practice tries to work around that. "Composition over inheritance" anybody?
Except that you want to share properties between those different contexts, and this is where hell begins. For instance (for a more programming-related problem) say that you have a gui application with graphical objects that map to your frogs,say a frog pond simulation. Now months passes and the boss says "okay, seems that our software interests molecular biologists too, but they need to have another 'molecular frog view' with properties Foobigate and Brozigate to study them properly at the molecular level. Of course the 'molecular frog view' has to be in sync with the 'pond frog view', also they want a simple readable save format.". Now, how do you set up your code so that Joe programmer that comes next week can be operational on the code in three days ?
To be honest, this sounds like a hell hole no matter what you did. It's a complex program that services two domains.
I would say that in the real world, a frog studied in a pond and frog studied at a molecular level share the same properties. A frog is a complex object that has many parts. Naturally for a pond view, you provide the properties needed for that, through composition of smaller objects. If you want to study it on a molecular level, you are really studying the properties, the constituent objects, at a much more granular level (and perhaps more that you didn't need in the simple pond view). It's all still one object.
In fairness, this would be much worse to model in bog standard OOP. Could Joe programmer fair any better there too? Would Joe programmer be working on a macro/molecular simulation in the first place?
What I'm saying is flip it on its head. Build an OOP language where you have features to make composition easy and separate the taxonomy as a separate thing that shouldn't be tied directly to an object.
I'm not sure what a full implementation of a separate taxonomy would look like. Duck typing with an algebraic type system that is only applied when you ask for it?
This is something I'm thinking about a lot. I've been wanting to sit down and sketch out what it would look like in terms of syntax / structure. Difficult with 3 kids, wife and a full time job.
I think behaviours should be able to specify taxonomy, which in effect is a name for saying "I only accept objects that do this and have that".
I envisioned a system where it's perfectly possible to work with purely anonymous taxonomies i.e your behaviours specify explicitly what its parameters have in terms of behaviour e.g if an object parsed a string, we could expect that the parameter has just the behaviours needed to do the work e.g length, substring, equality. Or we could just say it wants a string (which is just a separate alias that says an object has those things - like an interface except you don't have to declare you're implementing it).
I think there is value in being able to see an object you want to be part of a taxonomy fits the bill - this would be more like interfaces. Maybe it's better left to static analysis (stop! you're trying to pass an object that doesn't have behaviour X to behaviour Y that expects it).
Granted, something like duck-typing is more of a practice or pattern than something that's terribly well supported by the language (it's up to you to put safeguards that the object being passed to you does what you want it to do), but it's certainly possible and describes most well-written Ruby code.
What I described above wasn't duck typing plus responds_to - maybe it sounds like a whole lot of sugar on that. It was keeping the taxonomy separate, with behavioural sharing through composition only. I was elaborating on how parameters would work - do you allow taxonomies there or keep it separate still?
It's at odds with Ruby because it's still easier to inherit than it is to compose an object. As much as I love the language for its flexibility, it is still based on the idea of coupling the taxonomy with the object itself.
I'm sure an emulation of what I describe is possible to a degree in Ruby.
One of the opening pages of the Design Patterns book (the GoF book) has "Prefer composition over inheritance" (or words to that effect) as the only sentence on that page, right in the middle of it. My guess is that the GoF authors did that to emphasize the importance of that advice, maybe due to having seen too many brittle inheritance-laden hierarchies.
I mentioned this to a client I was consulting to, a couple of years ago, after I saw that he seemed to be using inheritance too liberally, and maybe without thinking if it was needed and the right thing to use for his specific needs. He did get the point, and changed his code from then on.
A lot of working programmers have figured this out already. Most experienced programmers I know don't use much inheritance.
Plus, my wider point was that in the real world, which is the inspiration for OOP, the taxonomy is artificial and placed onto an object. The taxonomy isn't an inherent part of the object (i.e Dog extends Canidae). The taxonomy is given to an object based on its composition separately i.e a dog is a dog because of some property x, y and z and just so happens that X, y are what makes Canid a Canid and X is what makes a carnivore a carnivore.
For example, an entity with a position and a velocity component (position could hold x,y,z values and velocity vx,vy,vz values) could be operated on by a movementSystem (which adds behaviour to any entities that have both a position and velocity component, and updates position values based on velocity values.)
The nice thing about such a system is that the taxonomy as you call it gets defined when you instantiate the systems of your program. If you have entities with position and velocity components but dont want to use a movementsystem to operate on them, you simply dont instantiate the movementsystem. Pretty flexible, but not used excessively outside of video games.
I was amazed when I saw the ECS architecture when I filled my toe into game development.
I remember wanting to apply it to other areas outside of game development.
It's interesting though that a paradigm that intends to help simulate the real world has been eschewed for speed and maintainability in favour of something like ECS in game development, which is all about simulation.
There's some great insight into how the pattern evolves in non-game projects here:
https://softwareengineering.stackexchange.com/a/306983/27143...
The system only operates on entities that have all the components they need. So there won't be one rendering system but several: one for entities that require movement, one for those which do not, etc.
Instead you would likely have an additional system that operates on entities that have both position and render components and propagates updates from position to render data. The render system for all entities with just a render component would then remain the same.
This can be imagined as systems that work on signatures (a component set or aspect that specifies whether an entity's components match the system's criteria).
A render system would have an aspect of say [RenderComp] and a positionRender system would have an aspect of say [RenderComp, PositionComp]. Note that the positionRender system would not actually render anything, only serving as a propagater of data.
Specifically, systems do not own components. Nor do they operate on full entities. Nor is there any coupling.
If you have entities that need to be rendered, you would give them a render component. A render system would render all render components that exist (thus 'rendering the entities' for whom those render components belong to)
If you have entities with positions but not render components, the render system would not operate on those entities.
Similarly if an entity has a render component but no, say, velocity component, it would not be registered to a movementSystem.
Systems that need to propagate data between components would have component signatures that include those components for which they need the data of. So a positionRender system would have an aspect of [PositionComp, RenderComp] and would be the only system to copy over data from one to the other.
There will always have to be a place that decides what object has which components. This should be as close as possible to the 'aggregate root' (a term from DI), and generally in ECS at the point of instantiation of the entity. I believe this question is prompted by a fundamental misunderstanding of ECS.
Now our componenents are nearly just plain-old-data and entities are slightly more sophisticated data-structures for tying them together. All the code is now kept is "systems", which are starting to look very much like plain-old modules.
No doubt the OO language will give you some useful tidbits, so that you can have slightly non-trivial getter setters and whatnot. But the design ends up fundamentally procedural -- which I say is good thing.
I actually think that it's more designed to reflect (one particular idea of) the way humans construct mental models of the real world.
> Real world objects are a composition of parts
At the lowest level, sure, but if our aren't modelling quantum physics, the “parts” are abitrary artificial divisions (or aggregations), not part of the fundamental nature of the real world, which are no more fundamental than taxonomy (or identity) of the objects they compose.
> In fact, the only reason why a frog is an amphibian is because it has parts all amphibians share.
In a typical taxonomy, that's true, but it's not in a phylogenetic one, which is based on ancestry, not parts.
> It should be entirely possible for me to construct an object that fits the definition of a frog without specifying I'm creating a frog.
OOP allows this (and it's common in dynamic OOP languages, back to Smalltalk), though static class-based OOP (which treats inheritance as type heirarchy) relies on declaration to limit what an object can be used as.
I was reading about the history of Simula and how the majority of OOP features originated there. It was built as a simulation language, hence the name. I took from that it was designed to simulate the real world - I guess based on the below that's akin to simulation of mental models.
I did briefly think about the quantum mechanics rabbit hole. You make a good point. I'll think about this more. People can tell what a radio is because they have a general idea of what a radio does - has a speaker, volume, aerial. This is what led my thinking.
I'll think more about this.
This is known as structural typing and TypeScript does exactly this. Two objects with the same properties are the same.
This usually works well, but there are a few issues with this. Number one is related to optimization. The type system that results from using structural typing is unsound. This doesn't matter for typescript since it compiled down to JavaScript which has no types anyways, but for a typical VM executed language there's many optimizations the compiler can't do.
The second issue with structural typing is what I like to call the "mixin problem". If you get too flexible with composition it becomes close to having no type system at all.
Like is all things which only have two integer properties considered the same?
{ Int age; Int weight; }
Would be equal to:
{ Int x; Int y; }
?
And this isn't always a good thing. If you have a function dealing with the property 'length' or 'value' for instance, you might as well have no type system because Typescript will allow you to pass in any object with those properties which is almost anything.
While the code will still compile and run like this the type system has failed its purpose of preventing developers from using the wrong types.
TypeScript is a superset of JavaScript and using "options" objects as method parameters is a common pattern. These options objects are synonymous with anonymous classes and don't have a name, only a shape. Structural typing is one of the strategies TypeScript uses to maintain compatibility with patterns in JavaScript that use these nameless objects.
There are no namespaces. Methods are compatible if they take the same or a subset of the parameters. Objects need to have matching property names and values.
I'm trying to wrap my head around it: if a method needs only the length property, which is an unsigned int, is there any good reason why it shouldn't accept any object that has that property?
It appears at first that this is a good quality: the code is general whilst obviously avoiding any type errors, including null/undefined errors.
Though people may argue on the meaning of OOP https://groups.google.com/forum/#!msg/golang-nuts/bSXry29pNo...
Direct link: http://www.purl.org/stefan_ram/pub/doc_kay_oop_en
One must remember that the competing paradigm isn't functional programming but imperative procedural programming. Very large code bases in C, Fortran, Cobol aren't all that nice either.
It's unfortunate that ML and Lisp didn't gain greater traction but that's likely due to Unix.
Many of OO's flaws are being addressed and are even missing from new languages. Rust "feels" OO but isn't. If you write C#7 or Swift and avoid inheritance and prefer immutable types as long as possible - how OO is your code then?
The problem that OO solved is that it allows mediocre coders to iterate on large scale code bases until a problem is solved. This is no small benefit in industry. The drawback that the code is hard to maintain and prone to bugs is an acceptable one.
Modern multi paradigm languages such as F# or Rust are really the way forward I think. The Java/C++ way of OO was a good but expensive lesson.
Both are 100% object-orientated (and in the case of Rust, also 100% FP is reachable I'd say. Not knowledgeable enough in C# to say). Do they have in-memory data structures with attached functions that do useful work with this object's state (not necessarily mutating it) ? Yes. Then OO.
Wait... do you actually believe that? Tons of C software is OO through and through, has been since C existed.
Rust has encapsulation, message passing in the smalltalk/OO sense, and polymorphism. It does this through objects with attached methods, and traits. In every way that actually matters to a working programmer Rust is OO.
If we stick to your definition C functions operating on structs qualify as OO, and I don't think many would agree with that.
What do you mean by "attached function"?
e.g. in a C API:
struct foo {
// dispatch
void (*do_stuff)(int);
int (*get_stuff)(const char*, void* context);
};
// attached functions
void foo_do_operation1(foo*);
int foo_do_operation2(foo*, int);
or C++ which allow to conflate both forms but restricts the first with inheritance struct foo {
virtual void do_stuff() = 0;
virtual int get_stuff(std::string, a_better_type_than_void_ptr&) = 0;
void do_operation1();
int do_operation2(int);
};
or JS where everything can change whether you want it or not var v = new Object;
v.do_stuff = function() { ... }
v.get_stuff = function(str, something) { ... }
v.do_operation1 = function() { /* v is available in the closure */ }
etc...
or OCaml modules: module foo =
struct
let do_operation1 = ...
let do_operation2 x = ...
end
in every case, some state is mentioned first, and then some code is bound to this state; then, the language (except in C where it is only a convention... even though some people can get fairly "creative" with macros) provides a way to call the code with a given variable of type `foo`.Lisp and ML are competing by nature(MLs are famous from their typesystems) and their "failure" is not because of unix, but because of their inability to deliver 'fast' & readable apps. Also, the learning curve...
True for freshman-year
std::cout << "Hello, world!" << std::endl
programs maybe. People's heads asplode when they get into C++ with any sophistication because they need to grasp several things at once: classes, inheritance and polymorphism, pointers (smart or not), value vs. reference types, etc. Java, JavaScript, and even Lisp go a long way toward hiding those details from the programmer.http://benchmarksgame.alioth.debian.org/u64q/compare.php?lan... http://benchmarksgame.alioth.debian.org/u64q/compare.php?lan... http://benchmarksgame.alioth.debian.org/u64q/compare.php?lan... http://benchmarksgame.alioth.debian.org/u64q/compare.php?lan...
Yes, it does. And those languages are all providing negative user/developer experience.
?!
Lisp under SBCL is as fast as Java under the latest Oracle JVM and for numeric computation is sometimes as fast as Fortran.
This means at least 10-20x faster than CPython, Ruby's MRI and others.
I routinely read third-party Lisp libraries and they are very readable, no problems with that.
Back it up.
> I routinely read third-party Lisp libraries and they are very readable, no problems with that.
Me too and they're all junk.
That's certainly true, but as someone who routinely has to go in and bugfix/maintain classes contaning thousands of lines of code (sometimes in themselves, more often collectively in their inheritance hierarchies), the simple presence of extensively used member variables often is a massive cognitive load. I'm not claiming that I'm some rockstar programmer who knows better, but from my experience working with other programmers, member variables might as well be called global variables.
tl;dr: Mutation, mutation everywhere.
The core concept of most OO designs is that data is coupled with the methods that operate on that data. There is absolutely no reason why that can't work with immutable data and pure functions. In fact, the String class in eg Java and C# works just like that: each method returns a new instance.
Scala, for all its warts and complexities, has a fantastic feature called case classes which encourage combining the best of OO with the best of functional. A case classes is both a class in the classical OO sense and an ADT. Most case classes are immutable.
This is easy enough to simulate in many other languages, even if they don't have case classes. For example, did you know Immutable.JS allows you to add methods to Record prototypes?
See my (geophile's) comment here: https://news.ycombinator.com/item?id=13850210
I've read this a lot, in this discussion and elsewhere, for years. But what does it mean? Does it mean data is syntactically coupled with state? Otherwise I don't get why pretty much any C program is an OO design by this definition.
Extra bonus though, compared to eg C, is that in OO languages you don't need to spell out the type (plus polymorphism ofc). So String_trim(&myString) becomes myString->trim() and that's shorter and, most of the time, equally clear and less noisy.
I miss this most when coding Elixir.
1) Java: starts out with one opaque string type, because that's what OO methodology says. Changes the internals when needed. All clients continue to work forever.
2) Haskell: starts out with strings as exposed linked lists of characters, because functional methodology says exposed algebraic types are OK. Sticks with this mistake for many years, then recognizes that opaque would be better and introduces a new type Text. Can't change the old one or make them compatible. Moreover the new type exists in strict and lazy variants, because Haskell methodology says people want that.
3) Rust: starts out with two string types, because the language design suggests both should exist (String and &str).
It's not just about strings. The same happens with compilation speed, binary compatibility, etc. "Modern" languages get it wrong and can't fix it, Java 1.0 gets it right and it stays right.
There are plenty of things that Java got wrong. Threads and concurrent programming in Java is particularly difficult to get right without expert knowledge. Haskell makes concurrent programming much easier. I've worked with both professionally and feel that Haskell got more things right.
I'd argue that java strings aren't perfect either, though. You can't seamlessly use the string interface for binary data or char arrays for instance. Also, java has various streaming implementations that all use completely different interfaces so maybe lazy java strings would have been helpful.
Basically, when you have the notion of ownership/lifetime in a language, you have to make a distinction between an owned and a borrowed thing. When you pass an owned thing to a function, you give up your ownership of it, and can't use it anymore. When you pass a borrowed thing, you keep the ownership, and the compiler tries to prove that, based on ownership info it has, it lives long enough for the operation to be safe. Just as String and &str are owned-borrowed counterparts, you also have e.g. Vector<u8> and &[u8] (the former is an owned vector, the latter is a read-only, borrowed view into that vector) and many more examples of this dichotomy.
I don't see how this could have been solved in Rust without having those two types.
We almost made str a library type but the downsides outweighed the upsides.
No, it absolutely doesn't. Functional methodology is absolutely orthogonal to data abstraction.
The same would have happened with FP. People would have messed it up too.
To take a concrete example, consider the undo-redo mechanism in a text editor. It seems natural to create a list of objects with each object representing the action that can be undone or redone, and a pointer to the most recent action object. Undoing executes the 'undo' method and moves the pointer to the previous action. Redoing moves the pointer to the next action and calls it's 'redo' method.
You would have a 'delete text' type of action which knows how to remove a particular chunk of text and put it back. You would have a 'change text colour' action which knows how to change text to a particular colour and revert back. You might have an 'insert jpeg' action which knows how to add a picture and remove it from the document.
Crucially, each action object in the list should encapsulate it's data and behaviour. It makes the undo-redo framework so much simpler if the framework knows nothing about each type of action, besides the fact that each action object has some kind of undo and redo behaviour.
I cannot see how you could get a simpler design without OO principles such as coupling the data and methods of each type of action, or without hiding the implementation details of each action from the undo-redo framework.
Most software with undo-redo will show you some metadata related to the action (for instance "Undo 'Drag stuff'", "Redo 'Set text in bold'"...) so you have to have a way to associate metadata with the "undo / redo" functions; the class is a good tool for this.
Also, a common optimization is to alter the head of the stack instead of deleting / creating new commands when for instance you are moving an object in the GUI. e.g. say you have a command that moves a box at a specific position. If command objects are immutable, every time the mouse moves, you have to pop the head of your stack and push a new command instead (which may be a waste of resources); instead you can just have an "update" method which will change the position stored inside the command which is microscopic in comparison of malloc / free.
Have you profiled that?! :)
Seriously though, design wise there was not much you said that was specific to OO. Take for example:
- all actions are a data type
- the history is a list of actions (in LIFO style for the purpose of simplicity here)
- undo pops the list and puts undo in another list
- redo replays the undo list onto the history list
A bit more outlined, but nothing OO there, it was in fact entirely functional.
actions = [append("The"), append(" quick"), append(" brown"), append(" fox")]
state = reduce(actions, some_combiner) // This might give you a string "The quick brown fox"
If you pop the last element `append(" fox")` of the actions and rebuild the state, you can easily see that the new state "The quick brown" is simply the old state without the last action, i.e. an "undo".If rebuilding the state is expensive, it is not hard to devise caches around state management under this model.
However, under this model, you really don't want your actions in the actions list to change after the fact. That means those actions in the list really shouldn't be modeled as "objects" with object-like mutable internal state and all. In fact, you'd probably need them to be as lightweight as possible, which means no sticking methods on them.
With those constraints in mind, OO-style code brings nothing to the table.
FP brings a lot of interesting concepts to the table that are useful. But I doubt that people that screw up OOP will screw up less with FP
In the end competent and disciplined (in my view the number one attribute of a good dev) people will produce good software with any paradigm. Clueless people will cause problems no matter what the tool is.
Caching becomes a lot easier though in a pure functional programming lang though, and in a lazy one happens automatically for a lot of things.
My code is pretty exactly as you describe, an undo and redo stack holding action objects and the interfaces to the action objects being nothing more than 'undo()' and 'redo()' methods.
Undo = [ { RemoveJpeg, JpegData} ]
where RemoveJpeg is a fun (like closure) and JpegData is what would be given as argument to this fun. The implementing undo looks like this: undo_action(Document,[UndoHead|UndoTail]) ->
{UndoFun,UndoData} = UndoHead,
UndoFun(Document,UndoData).
Isn't it simple? RemoveJpeg just returns modified document. undo_action knows only that every action should take two arguments and return modified document. RemoveJpeg <- [:data | ...].
JpegData <- [:data | ...].
Undo <- #( RemoveJpeg, JpegData ).
As possible implementation of undo could be undoAction: document with: undoList
undoList do: [:aBlock|
aBlock value: document].No one stops you writing program in whatever style you want. If functional programming has that of edge over OO, it would win over the argument long ago.
I don't like you assume other people don't know what is best for themselves, and needs some top-down expert to light the path. You argument, if there is one, sounds really preachy and proves no other than the point that support of functional programming here in HN are more about identity politics than actually making it useful to the crowd.
I do love the juxtaposition of this line, with your following accusation of people on HN engaging in "identity politics".
In my view the argument "X can't be better than Y, otherwise we'd all be using X". Is wrong for the same reason that "K can't be a good idea for a company to start otherwise someone would've started it already."
Some things are too early, some things are marketed poorly, sometimes it's luck, some times is dedication in sticking with it. I strongly believe that markets aren't efficient and have lots of imbalances. I also strongly believe that life isn't efficient and has lots of imbalances.
But it illustrates the issues with this sort of work. This is really about C++ vs C/Pascal and not OO vs Proc. Also this is 1997 C++ which I earned a living from for a brief moment, and I can attest, it was horrible, and very different from 2017 C++.
From my perspective today : we need to separate parametric and polymorphic oo in our thinking and the big problem is that programmers use these tools to the limit (creating objects with 3+ parameters or deep inheritance hierarchies) and weave silly complexity into the code base .
In a commercial setting the biggest impact I have seen is a project that became impossible to staff because as soon as the developers saw the code base they started looking for other work. It was really good code developed by a two brilliant people initially and then three or four more smart ones as the project grew, but when the lead left it was just too complex - due to prolific use of generics and a obsession with ORM.
I can imagine the Smalltalk community rolling its collective eyes and saying "that's not OO..."
My issue with the primary study is that the two programs are not really comparable: a think a C++ parser was a harder problem than a C parser (even given that it is 1997's C++), and this is backed up in the study:
"The C++ parser by further contrast is a recursive descent parser owing to the intractability of parsing C++ using 1 token-lookahead grammars. In other words, they are very different products..."
In mitigation, it says that the C++ parser, as used in the comparison, did not parse the full language, thereby presumably reducing the difference in the complexity of the requirements, but I think the difference in architecture alone is enough to raise doubts over the significance of the results.
[1] http://wiki.c2.com/?AlanKayOnMessaging [2] http://worrydream.com/EarlyHistoryOfSmalltalk/ [3] http://squeak.org/ [4] https://pharo.org/ [5] http://www.infoq.com/interviews/johnson-armstrong-oop [6] http://thinkrelevance.com/blog/2013/11/07/when-should-you-us...
In contrast, functional programming makes state explicit. This makes it easier to manage and control state.
In what way does functional make state explicit? If I look at a structure (if you have one in your language) and ask who changes this part of it across a large code base - it's much worse when you've just got a bunch of functions (with bad names) which can be hiding anywhere.
And the "reasoned about" shibboleth. What "reasoning" do you actually do.
Don't get me wrong - I have a long history in Lisp and ML and they're good languages in which good people can write good software. But no magic bullets.
In another part of the development world I've seen React programmers jumping on the functional bandwagon and deciding that all components have to be stateless. Taking the state outside the components means that we have lots of bugs with mistaken state sharing. So external state doesn't equal well-managed.
The exception is the IO entry point.
There is a lot of opportunity to sandbox things so that if you know the type of the function you know what it can or can't do to the programs state.
That's kind of the definition of encapsulation.
> difficult to reason about
This is repeated as a mantra without any backup. Most people find state trivial to "reason" about. What becomes more difficult are certain types of proofs that nobody actually does.
> [FP] makes it easier to manage and control state
No, not "easier". In fact, incredibly more difficult. Just try updating a nested member. What it does is make it easier to isolate state updates, because it makes it so arduous.
State encapsulation, at least in FP, means that the state is not visible externally. For example a pure function that uses mutable state in its implementation. Such state does not contribute to the sytem global state.
> This is repeated as a mantra without any backup.
The more global mutable state your application has, the harder it is to reason about. That should be fairly obvious I think.
> And most OO-languages have, apart from the assignment operation, no abstractions for
Yep. "Once you're inside an object, it's essentially Pascal" -- David Robson
That's what Polymorphic Identifiers[1][2] in Objective-Smalltalk[3] address. Partial inspiration was the Web and its REST model, which shows that using state as the basis for composition is not just possible, but works really, really well at the largest possible scales.
What it does is replace your bog-standard programming language identifiers with URIs. So you can write things like the following:
comment := https://news.ycombinator.com/item?id=14997098
file:/tmp/comment := https://news.ycombinator.com/item?id=14997098
file:{$HOME}/comment := https://news.ycombinator.com/item?id=14997098
file:{$HOME)/comments/{id} := https://news.ycombinator.com/item?id={id}
But that's not the cool thing. The cool thing is that scheme handlers are completely user-defined and composable, so you can abstract over state handling, with scheme combinators that take other scheme handlers as parameters, for example a cache, or filters.Oh, and references (ref:comment, ref:file:/tmp/comment, ...) generalize the concept of pointer. So creating a symbolic links becomes:
file:link := ref:file:path/to/link/target
etc.[1] http://dl.acm.org/citation.cfm?id=2508169
[2] https://www.hpi.uni-potsdam.de/hirschfeld/publications/media...
EDIT: the reason why there is an explosion of state is because every object in OOP is potentially stateful. If I build a composite object Z from objects X and Y, the set of possible states Z has is a product of the set of possible states X and Y have.
I'd be interested in knowing what theory you are basing this on. In practice, I have never seen this combinatorial explosion of state, but maybe that's just me.
What it lacks is a multiline anonymous function syntax.
See PEP 227: https://www.python.org/dev/peps/pep-0227/
From the link you posted: "all uses of the name within the block are treated as references to the current block"
The Python folks call them "late binding closures" and they are frequently discussed as "gotchas".
Essentially lexically scoped variables are captured as references to mutable state. Java did not make this mistake, perhaps because they had Guy Steele (of Scheme fame) on the committee. It is a questionable default that ultimately just destroys ones ability to reason about programs.
That's how you implement "objects" using lambdas. You make closures that mutate their closed-over bindings.
But the problem with program comprehensibility comes from doing these gymnastics with block scope and anonymous functions in the first place. The right thing to do is to give things proper names and interfaces.
In other words, both what Scheme does and what Python does are bad for reasoning about programs, precisely because the semantics of what should be done in this scenario are implicit and unable to be discerned solely by reading the code. Since either behavior is non-obvious, and understanding the program relies on this implicit behavior, the practice of mutating a variable while repeatedly creating closures over it should be avoided.
Scheme's version of the do loop actually performs fresh binding, rather than mutation, so lambdas in the do loop capture the iterations separately.
If you mutate a variable with set!, all the closures over that variable see the change.
If that destroys one's ability to reason about programs, then one never really had the ability to reason about one's programs in the first place.
It's not like nested block scopes are some boon to program comprehensibility anyway.
Well, yes there actually is: the Lisp 1 language that MacCarthy and friends developed on the IBM 704, and its immediate follow-ups through Lisp 1.5. Since that, there isn't.
Emacs Lisp, ISLISP (which is defined by an ISO standard) Standard Lisp, Common Lisp (which has an ANSI Standard), ...
Languages in the wider Lisp family, which are more or less incompatible (Logo, Clojure, Dylan, ...) don't use Lisp in their name.
Yes, they can. In ML, you can build a function that returns a set of closures that manipulate or use shared mutable state. I'm quite sure you can do something equivalent in Haskell, too.
As always, “it depends” whether you would actually want to do that.
Eh, no it doesn't. I think you need to expand on that statement because it isn't remotely true in my experience.
Granted it has been getting easier as OOP languages have been slowly getting more and more functional features. But it really is easier to favour immutability and composition in a functional-first language.
This is exactly one of the problems encapsulation and abstraction aim to solve, and has nothing to do with OO: you could also create a combinatoric explosion of possible behaviours by dynamically composing pure functions.
For example, the amount of articles about programming languages like Rust or Elixir is disproportionate compared to their actual relevance in the industry.
HN has a very strong bias for functional programming.
You almost never hear about Go or Node.js anymore except when there is a major release but these languages are extremely widespread and proven and there is a lot of very interesting stuff happening in these communities that HN seems to ignore.
It's just the nature of the industry (and people in general). New and different is always more interesting than old but incrementally improved.
But not to their possible relevance.
There are lots of little rules in place to minimize the damage of this (like no memes, jokes, pics, etc.; HN actively discourages clickbait), but it can't force people to actually read the articles before responding. And, one doesn't have to read the articles to vote, either (again, HN mitigates that problem somewhat by requiring a minimum level of upvotes before being able to vote, etc.).
It's hard to have a substantive conversation online when so many of the incentives are for responding quickly (and probably without going too far against popular opinion within whatever community you're in).
They could hash the title for a minimum time, or until at least three comments are posted.
That would at least force people to click the link to find out what's there, and filter out commenters too lazy to do even that.
So, I think that hashing titles would make it so that the worst (by some definition, but one that probably matches HN's definition) clickbaity stuff would rise to the front page faster and more often. This would happen because people could no longer make any decisions about what to click on New by it's title, and so the short articles that confirm common tropes, the funny pictures, etc. would get people coming back faster to click the upvote, while the longer articles would be more slow to get upvotes.
Same problem we have already, only moreso. At least now, we have a stage where we can see the title in New and the interesting things might get upvotes sooner...you have a little more knowledge going in. i.e., I might not read the whole article about something I find really interesting, but I might click over to it, see that it's by someone I trust or seems to be well done and free of fluff, and upvote it before finishing reading.
I dunno. I could see blocking commenting for a time after a post. But, hashing titles seems to just take away valuable information people use to make upvote decisions during the vital New phase of a post. Also, there's no way I'm clicking on every link on New. New is a goddamned wasteland; 50+% garbage, sometimes.
It'd probably generate a lot of false negatives, though. Even humans make bad comprehension tests sometimes.
Easiest implementation is that the article poster gets to define the question and answer. In the future, an AI could take over.
OO (in this case the author means C++) is less related to a programmer's understanding of reality compared to C because... programmers of the 90s make more errors that are harder to fix in C++.
Yeah. Right.
But, it is reasonably hard data, and data has value, even if the interpretation is up for debate. I think if everyone had read the article, that'd be the more common theme of the discussion here: Are the conclusions valid based on the data seen, and either way, is it useful to know...is there something actionable in this data?
This is the same as the title, and enough for us to discuss. I want to hear what people here have to say about the subject. That's a lot more interesting than what this 1997 paper has to say on the subject. (Of course, people can comment about that as well.)
Without diving deep into the uncanny valleys of existential philosophy surrounding trying to be both observer and observed, there is a big leap from I am thinking in objects right now to Everyone always thinks in objects [or something approaching it].
Although by your standard our code would then match the way we think, clearly this is not a useful takeaway.
> If writing is thinking, then object oriented code reflects the way we think because we write it
The point is the huge majority of systems (by any metric; number of engineers, lines of code, budget, ...) are written in an OO style (regardless of whether the language actually supports OO; practically all bigger C projects use OO); and the majority of people are just fine with that and get started fairly easily with it. Hence it is reasonable to assume OO being a good fit to our own thinking processes.
By comparison, very few people find learning FP easy, and most seasoned FP developers openly say that getting started and acquiring the mindset for FP is a difficult and long-winded process.
Similarly, most people find reasoning about point-free programs (concatenative languages such as Forth) quite difficult.
You also made your point very well, and all the facts you highlight support you and are good reason for me to doubt my conclusion regarding object oriented programming. I thought it's a terrible match for how people think about the world yet you give very strong reasons why I was wrong. (In other words you've convinced me.) Thank you.
Most novices do not in fact find OO easy to learn, and most novices do not in fact find FP harder than OO to learn. What you are probably alluding to is the difficulty in learning a specific language, like Haskell. Haskell is not FP.
Also, while most software projects use some kind of OO language these days, I don't know why in the world you'd think this somehow entails it fits our thinking. By all accounts, software defect rates aren't any better under OO than under procedural language paradigms. How does that make it intuitive?
To make any kind of "reasonable" conclusion, you'd need a comparison against software written under other paradigms. Such studies have been done, and OO does not fare as well as other paradigms on many metrics.
To me, it seems more likely that the flexibility of human cognition allows a person to think in terms of a programming language than any particular programming language expresses something important about the mechanics of human thought. Another way of putting it is that the claim that object oriented programming is ontologically different from other Turing complete languages seems less believable than ontological equivalence -- the Chicken [1] interpreter is written in JavaScript.
With the advent of computer languages, some of these have captured more of this independent thinking and abstraction, others capture less of it, while still others require their users to learn abstractions they were not used to.
The idea that all of this falls in the last category, with no capturing of the way "people think independent of the use of that language" is totally false to me. Sorry.
Writing is thinking...etc.
There are non-linguistic modes of thought that more naturally capture our intuition of evolving systems. You don't linguistically think through a baseball play in the moment. The most intuitive programming paradigms are actually event-driven [1].
To summarize, you can compute by thinking and you might be able to think by computing, but thinking is not like computing. Any particular programming paradigm is a mode of computing, not thinking. A programming paradigm that more closely aligns with thinking is more intuitive and natural, and will lead to fewer errors.
He also had a couple of follow up ones.
Basically he blames java for oop's popularity. He maintains that oop's combining of methods and data is actually a fault and not a benefit. That every good oop promises apart from inheritance (which is bad anyway) is more easily achieved in procedural programming. OOP promotes endless meaningless abstractions because the problems programmers deal with are not amenable to neat abstractions. So while oop looks great when dealing with animals, cats and dogs, in real programming you end up with an explosion of difficult to name classes, the so-called 'kingdom of nouns'.
As for the OO itself, I think the problem is that any poorly written non-OO code typically goes unnoticed or just labelled "bad code", whereas poorly written OO code is labelled as such: poorly written OO code. They both are equally bad. We need to accept that if something is wrapped into a class or even some hierarchy, it is by itself not a guarantee at all that it can be useful or functional.
Not sure what you mean. Do you mean oblivious?
Linus Torvalds has even gone on record to express his distaste for C++ and OOP.
I'd go as far as to say that every time you have a C API that looks like
struct some_struct { ... };
void my_api_foo(some_struct*, param1, param2);
int my_api_bar(some_struct*);
float my_api_baz(some_struct*, float**, int n);
it's OO. And the kernel is full of this (just like many C APIs).Also, Linus's arguments are about C++ usage in kernel. His diving software Subsurface was ported from GTK / C to Qt / C++ for instance. (and even GTK with GObject* everywhere is fairly OO).
Similarly that article is wrong to imply that all these techniques are unique to OOP.
OOP languages formalized this pattern and made it easy to use.
OO just means that you have a semantic entity, the object, which contains data and to which you associate code. Your main tool to work with is this object; just like in FP your main tool to work with is the function to which you associate data through closures and currying. Likewise, in logic programming your main tool is the constraint. And these aren't relevant to the language. You can do both OO and FP in most mainstream languages.
> dynamic dispatch
is just allowing to associate a different implementation to the same function name. Every languages that have some kind of function pointer allow this.
> polymorphism
is in almost every language, including FP.
data Tree a = Empty | Node a (Tree a) (Tree a)
is polymorphism. C++ templates are polymorphism. Rust traits are polymorphism.> message-passing
is only relevant if you adhere to Alan Kay's vision of objects. I personally don't (even if the guy coined the term).
> Every languages that have some kind of function pointer allow this.
It doesn't seem like you're reading the discussion. The claim the parent made was that "every time you do {X}, it is OOP". My reply was "yes, you can do OOP in C, but OOP implies far more than just {X} (it implies {Y}, etc.); merely {X} does not imply OOP".
You reply with "languages that have function pointers allow {Y}". Well yes, they do allow {Y}. Most/all of them in fact probably allow OOP in full. Nobody suggested such languages don't allow OOP (in fact I said the opposite about C). What does that have to do with the entire discussion and argument? Your argument isn't even wrong... it doesn't even compile.
> message-passing is only relevant if you adhere to Alan Kay's vision of objects. I personally don't (even if the guy coined the term).
This 100% completely misses the point of what I said. Replace it with "procedural call" if you're allergic to "message passing". The point I was making was we're talking about subroutine calls: you call a procedure and wait for it to produce a single value as the resulting output before proceeding. Again: if you're not used to other paradigms then that might be why you're missing my point here and adversely reacting to superficial things like the nomenclature. Whether you dress it as message passing or anything else has nothing to do with the issue.
maybe it was not clear, the point I am trying to get across is that "OOP implies far more than just {X} " is incorrect; OOP doesn't imply any of inheritance, dynamic dispatch, run-time polymorphism, etc. I took function pointers as an example of why "supporting dynamic dispatch" is irrelevant for categorizing programming languages as OOP / not OOP (and I would even say that doing such a categorization is in itself a worthless idea).
> you call a procedure and wait for it to produce a single value as the resulting output before proceeding
I must admit my ignorance : which languages except prolog don't work like this ?
Again, like everywhere else in your comments, you're conflating and confusing the language with the model/paradigm and keep trying to shove a language into a programming paradigm. Firstly, you can do OOP in assembly and yet it's not an "OOP language". The language is quite independent of the paradigm. Secondly, who was even trying to categorize languages here? You keep thinking the argument is about categorizing languages and then refute a nonexistent discussion...
> I must admit my ignorance : which languages except prolog don't work like this ?
Again: the discussion is not about trying to shove languages into categories; it's about programming paradigms. You can use the same language to code in multiple paradigms.
Look up "actor-oriented programming". It's a different model meant for concurrent processes, i.e. it's not a subroutine model since you don't need to wait for the procedure to finish and produce a result before continuing.
There are lot of "models of computation" out there (probably a better search term than "programming paradigm", btw) and people go so far as to do PhDs on these. I suggest looking around and not assuming everything is some trivial variation of OOP or FP or DP (declarative programming) or whatever you saw in undergrad.
What is object oriented programming? My guess is
that object oriented programming will be in
the 1980s what structured programming was in
the 1970s. Everyone will be in favor of it.
Every manufacturer will promote his products
as supporting it. Every manager will pay lip
service to it. Every programmer will practice
it (differently). And no one will know just
what it is.
Tim Rentsch, "Object Oriented Programming", SIGPLAN Notes, v17, n9 (1982).Object oriented modelling means creating a model of a resistor, or a diode etc., and then using it in a different models. For simulations, such inheritance is exactly how humans think, and exactly what makes life easy.
If I'm simulating an air conditioning system, I create a class of compressor models, that correctly abstract the equations that most compressors have. I can then derive from that base class and reuse for more complicated models and eventually subsystems of such models. Each model has been debugged by itself, and its reuse is a matter of a few "connect" statements. Further, I can have classes of, for example, compenent geometry or state parameters. This makes thinking about the problem much easier.
This is exactly why Modelica is eons better than Python or Matlab/Simulink for writing good simulation code for certain cases. The "solve a problem only once" aspect is handled fantastically in object oriented modelling.
* Write an initial mockup that exercises APIs and data paths in an imperative, mostly-straightline coding style
* Write a source code generator that reproduces part of the mockup by starting with the original code as a string and gradually reworking it into a smaller specification.
* Now maintain and extend the code generator, and write new mockup elements to develop features.
The mockup doesn't have to be 100% correct or clean, nor does the code generator have to be 100% clean itself, nor does 100% of the code have to be automated(as long as clear separation between hand-written modules and automatic ones exists), but the mockup is necessary as a skeleton to guide the initial development, and similar, comprehensible output one layer down is a design goal. Language-level macro systems are not typically sufficient for this task since they tend to obscure their resulting output, and thus become harder to debug. Languages that can deal well with strings and sum types, on the other hand, are golden as generator sources since they'll add another layer of checks.
I'm still only using this for personal code, but it's gradually becoming more "real" in my head as I pursue it: the thing that stopped me before was developing the right feedback loop, and I'm convinced that the way to go is with a pretty lean base implementation(Go is an example of how much language power I'd want to be using in the target output) and an assumption that you're building a bespoke generator for the application, that won't be used anywhere else.
Source code generation gets a bad rap because the immediate payoffs are rare, and it's easy to take an undisciplined approach that just emits unreadable boilerplate without gaining anything, but the potential benefits are huge and make me not really care about design patterns or frameworks or traditional "paradigms" anymore. Those don't achieve anywhere near the same amount of empowerment.
I didn't know opinion pieces could be disguised as academia. Of course the article has some interesting points about human memory, but that's why we have things like single responsibilities.
If you disagree, point to a definition of OO that you think is "correct".
Further, that email thread is far from being a formal definition. In that email there's a pointer to a ISO standard doc, but that's hardly authoritative either.
It seems like every language (and every programmer) has their own idea of OO, which is why I consider it quite apt to call it "ill-defined". That's not to say that OO programming itself is bad, more that the term "OO" is bad.
I would argue that single responsibilities is nice to have. However, OO-style objects are responsible for both data and behaviour. Object-oriented languages themselves violate the SRP, to its own detriment.
Both functional and imperative languages tend to encourage separation of data and behaviour. I can operate on the same data structure implemented under a different context, without having to bring in its unrelated set of methods. This leads to cleaner code reuse and looser coupling - both nice properties to have in a complex codebase.
"When the tool you have in your hand is a hammer, every problem looks like a nail..."
Things like inheritance, polymorphism, dependency injection, and "abstracting things away" in general have made it practical and common for implementation details to evaporate in such a way that we don't (and maybe can't) keep them in our heads when we are maintaining existing code.
This is great if all those class hierarchies and abstracted implementations are bug-free and work the way you anticipate them working (i.e. if they fit your mental model). As soon as they don't, or they get too complex and too deep, you're in for a world of hurt and confusion.
class Door {
void open() {...}
void close() {...}
}
door.open().
But can a door open itself? Maybe Man petya = ...
petya.open(door);
But can a door be opened if it's not installed? So walls should also come into play when we think about door and its methods.It's difficult to model the world with OOP (at least in its current state).
When you say a door can be opened, you are making the assumption that the door is installed in some sort of building. But then, the door is a component of that building, not just a stand-alone door. It can only be opened in that context.
So I would suggest something like this:
class Door(Material, Size)
class Room(List<RoomConnection>, Size)
enum DoorState { Closed, Open, Locked }
class RoomConnection(Room, Room, Optional<(Door, DoorState)>, Size)
To open the door, you don't need a method in any of these "data" classes, you just need to update the door state given there's a door in a room connection you're interested in.The room connection may or may not have a door, and that could change over time. If it does have a door, then the door must have a state (but not all doors have a state, only the ones that are in a Room Connection, possibly others).
Notice that to update the room connection, you don't need mutable state locally... you just need a way to provide a new RoomConnection to code where opening a door may be possible (normally, you're given a RoomConnection as an input, and you might give a new one, basic FP style but OOP can also operate in that way).
So, I don't think it's that hard if you know what you're doing.
I don't want to even comment on the suggestion to have a `Man.open(Door)` method :) that's really not good.
For example, why Room?
To that end, can someone point to me an example of object-oriented code that they feel is elegant?
I wrote an abstract base class that contains all of the boiler plate code for dealing with SharePoint, and then one subclass for each type of message I need to deal with; those subclasses only need to override a few methods to deal with the fields from the XML message and with the specific SharePoint list they need to write to.
It is not great (the task is just too dull for that), but I am happy with the approach I have chosen because I can add support for new message types fairly easily.
That being said, I do not think OOP is the answer to all (programming) questions, nor is it the worst idea ever. Some programs can be expressed in terms of objects and inheritance very easily, just as some can be expressed in functional terms very easily.
I worry as well about the push for FP to somehow 'replace' OO, when really they address different concerns, and can actually complement eachother.
If you work in Java, everything is contained within a class. You can't easily think outside the OO box because the language insists that everything you do is done in terms of OO, and this has great implications from the standard library, to primitive type boxing. It's very much constrained by the prevalent trends of the mid-90s. C++ is a more OO agnostic, it being something you can opt into as and when it makes sense.
I certainly don't think OO is a "dud", and it's provided a conceptual framework for programmers of wildly varying abilities to create and maintain vastly complicated codebases. But... it's only one way of many to structure and reason about program logic and data, and to constrain oneself to only using OO is greatly limiting. Now the hype has died down, I hope we can use OO where it fits, and avoid it where it does not, rather than as a blunt instrument for every problem?
The main problem I see in my day to day work (imaging related) is that OO is too costly. Arrays of object instances are too cache unfriendly. And code doesn't always need to be directly tied to data. I see Java code using collections of primitive arrays in place of objects because most OO languages don't provide a means of laying out objects in column stores, even though it wouldn't be technically difficult (just different offsets to members rather than packed structures). Treating objects as individual private collections of state leads to only being able to work with an object-centric rather than data-centric view, which can be quite detrimental. It's this view that leads to abominations like database object mappers, treating tabular data as object collections when they are not. While such things are possible and even popular, they often come with significant tradeoffs. I've encountered developers who are completely constrained within an OO worldview and can't take the blinkers off and see what's possible outside the box.
Also functional programming has its benefits.
The 1990s/2000s with the OO hype around C++/Java/C# was a set back.