Pretending OOP Never Happened
johndcook.com
johndcook.com
But it seems to me a more natural scale to use is how the state is managed. In procedural programming state is global, which is messy. OOP cleans it up a bit by splitting global state into pieces and making them local. Functional programming takes this even further by avoiding having to reason about state altogether.
This is a natural way to work on procedural code (in an iterative development fashion) to move from prototype/early code to stable, maintainable code. You create global variables, then encapsulate them in a record (perhaps keeping just one record globally), then you move to passing a record around (permitting multiple states to exist side-by-side). Ideally you'd start with passing the record around, but sometimes you just write code to get the concept implemented and refactor from there.
All programs have state. It's worse if the state is shared. It's worse if the state is mutable. It's worse if the state is global. It's the worst if it's all three.
Functional program still have state. They typically don't have shared state, or mutable state, or (much) global state, though. Those are real wins.
Mirror of that is a driver for a piece of hardware. You got multiple levels of state going on.
1. To try to force restrictions on someone who doesn't understand the reasons for these restrictions will not magically ensure good results.
2. To try to force restrictions on someone who does understand the reason for these restrictions is just a pointless nuisance.
https://www.gamasutra.com/view/news/169296/Indepth_Functiona...
For example, I have an object... what do I do with it? So I hit the '.' on my keyboard and read the list of things that the object can do.
This is why OOP is popular, it allows better tooling. If people want to move beyond OOP then they need to think about how the new style integrates with the IDE so that the programmer can hit the '.' or equivalent and get a list of useful things to do with the object in question.
Haskell has a powerful enough type system that it can enable "hole-driven development" where you specify the type of your code and then leave a "hole" in the implementation -- anywhere in the implementation, including making the entire implementation a hole! -- and the tools can suggest what should go in the hole, leaving new holes in the code.
Then you can incrementally resolve smaller and smaller holes based on the suggestions you get, and finally you have a completely tool-written program, where you just helped disambiguate when you had to.
This is the '.' on steroids.
Haskell is not popular.
"While >>= and return are the basic monadic sequencing operations, we also need some monadic primitives. A monadic primitive is simply an operation that uses the insides of the monad abstraction and taps into the `wheels and gears' that make the monad work. For example, in the IO monad, operators such as putChar are primitive since they deal with the inner workings of the IO monad. Similarly, our state monad uses two primitives: readSM and updateSM. Note that these depend on the inner structure of the monad..."
Or, possibly because when I make a post on Hacker News about how difficult it is to implement Quicksort in Haskell, the first response is a four-line Haskell implementation of Quicksort, spawning a 50-post subthread which points out that it's not actually an implementation of Quicksort, because it does not have the same performance characteristics as Quicksort, and instead has the performance characteristics of a lethargic water buffalo.
By the end of the thread, the question of whether it is possible to write a Quicksort in Haskell that has the performance characteristics of Quicksort is left as an open research question.
Jane Street uses OCaml, but of course that's not Haskell. All the other finance firms seem to use pretty standard C/C++ because they need the low latency.
Not really. This is all still painfully manual.
Both type hole development and auto completion are enabled by static types. Haskell could theoretically benefit from all these advantages that mainstream statically typed enjoy thanks to IDEA, Visual Studio, or XCode but the Haskell community has this arrogant attitude that they are above such "pragmatic" tools and as such, never took the importance of IDE's seriously.
So when you watch a video on Haskell's awesome type hole development, the developer is often using vim and typing everything by hand. In 2020.
That's just dot-syntax and modules. That has nothing to do with OOP itself. Nothing is keeping you from doing that in functional languages that support proper modules.
As a counterpoint (relevant Rich Hickey rant): https://www.youtube.com/watch?v=aSEQfqNYNAc
Non-trivial data structures are always conformed to some specification, doesn't matter if its Clojure or an OOP-language. If you receive an HTTP request over the wire you will have to check it. Once you conform it to the specification you know exactly how it looks and there's no need to jump through hoops to access the information.
I have a lot of respect for Rich Hickey and I feel like he Understands something that goes straight over my head, but his appreciation for generic, shapeless maps and lists is something I just don't get.
Its construction is dependent on it conforming to a specific form. In this case, your statically typed data comes from a parsed string that is then conformed to a specification to ensure that is has a certain shape. In Clojure you would just do the conforming to specification part and keep the data generic.
I think the appreciation for generic data has to do with the fact the fact that it makes Clojure code very generic and facilitates a lot of functional composition.
And I would assert that every maintainable C program is fundamentally designed in a relatively object-oriented way (functions that operated on structured, polymorphic function pointers, etc.) even if the original author wasn't particularly thinking in terms of classes or inheritance.
C programs aren't functional because C isn't functional (no inherent concept of closures), but Greenspun's tenth rule: Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp" usually applies.
i mean, the fact that you can use class style syntax do define object constructors in javascript and tho result is fairly coherent, seems to suggest that classes are not inherent to oo.
also, the fact that it's called “object oriented” not “classical”.
other examples of non-oo with strict encapsulation includes haskell, where if you lack the constructor, you can only use it according to the public api. it also uses single dispatch for typeclasses.
complex inheritance rare in oo - mostly only single inheritance is supported - and most oo design advocates pretty much say “don't use inheritance”. so it doesn't seem like it's intrinsic.
You don't need closures to do functional programming.
I was recently tinkering in some C++ code and thought "this part should be able to run in parallel." It was simply doing the same operation of a list of objects. It's a complex operation involving some temporary data structures. I had to dig through several layers of functions, but I kept finding that all data was created locally. Functions that returned complex result are passed a class to fill out. It was then that I realized it was written in a functional style - or at least my limited understanding of what that means. The top level function is now run in parallel, which required no changes beneath that level.
Now I prefer that style but like TFA here, I don't think 100 percent functional is a good idea.
That is in the eye of the beholder.
I can well believe that you would look at the code and decide that the author wrote in an OO way. I also believe that there are plenty of authors of such code who would disagree with your assessment.
There are other ways as well and all of them work well for certain problems and not so well for others. Limiting yourself to only one lense with which you look at problems is a bad thing.
Well, agree, but in my experience people who abandon OO are usually the ones who are limiting themselves to only one lens - the top-down/procedural way software was developed in the 60's and 70's which is easier to comprehend but ultimately unsustainable for subtle reasons. (not saying that's you, just saying a lot of people do).
Rejecting OO in favor of FP like Paul Graham does? Great! Hopefully you'll make some headway and functional languages/styles will really catch on!
Rejecting OO in favor of one big function that handles everything and a few global variables (like nearly everybody who uses the Spring "framework" abomination in Java)? Maybe spend some time considering why, if it's so obvious, it's referred to as a "bad practice".
I also don't get the Spring hate and what that has to do with creating "one big function and a few global variables."
I do the same with object-relational mapping--write a thin layer of classes to wrap INSERT, SELECT, UPDATE, DELETE. Not hard to write, easy to test, and you can always slip into SQL if you run into problems.
Use-case specific code should always be considered as an alternative to general frameworks.
Do you use Dagger for your DI needs or do you roll your own container?
Outside of class based inheritance which aspects of Object Oriented is Go missing? I code Go daily and consider it primarily an OO language.
I don't consider inheritance an important feature for a language to be an "Object Oriented Language." Inheritance: none, single, and multiple have been bandied about in the OO world for quite some time now so I understand where you're coming from.
I break out design from the language. While I wouldn't design very deep without knowing the language I was targeting I do consider Go as having a natural bias towards driving me to OO design over, say, functional or procedural. For me Go lends itself naturally to OO design. Sans inheritance, of course.
> "if you don't have these, then OOP is indistinguishable from FP or DOP. If Go is OOP, then which languages aren't, and why?"
Haskel, some Lisps, some Forths, etc. would be more non-OO languages in my mind. C is also a non-OO language to me even though you can certainly code it in an OO style.
I guess I agree with the blog author, OO is simply part of the language landscape these days and isn't going to be supplanted by functional any more than procedural was supplanted by OO. And with my mind-set then I'll say most all of Algol's recent children can be considered OO languages. So for me Go is definitely an object oriented language. But I now understand why you don't.
I mostly agree except for one catch. How do you extend a class which is encapsulated properly? Often this is the only way I use inheritance because I need to do one or two extra things that rely on the internal state of an object which I can't modify directly. So I'm wondering if there is a good way that doesn't rely on modifying the implementation of the original object to be more flexible.
Going back to the "old days" you'd reach for a pattern. Adapter pattern was the most common. Bridge or Facade maybe? But patterns fell out of favor a while back (imo) because they were tough on the coder's mind. A lot of languages added useful bits to create these pattern behaviors more naturally and without reaching for a book. I'd need to see code to be more specific.
So I'd rephrase that I suppose, perhaps we don't need something called Inheritance, but we do need a way to extend objects we don't control without breaking encapsulation.
As an example if you had a method that outputs XML and you want JSON, you can use an adapter object to call it, read the string, and reformat and pass it through. That works (and I've done things just as bad or worse many times from necessity), but it's definitely more hackish and less efficient than a solution that uses object fields to output JSON natively. Inheritance is my familiar way to do this in cases where I don't control the code (in which case injecting a Writer class of some sort probably makes more sense).
I'm not advocating against inheritance. If it works then go for it. I do when I'm coding in languages that have it. This stemmed from a thread discussing whether the lack of inheritance in Go was sufficient to exclude it from being an object-oriented language.
By definition, you can't extend objects you don't control without violating encapsulation. Encapsulation is all about controlling access so that the owner of a class can make changes without breaking downstream code. If downstream code accesses these private APIs, the downstream code may break, regardless of whether the access was via inheritance or composition (as a side note, "protected" makes no sense--who cares if downstream code breaks because it was accessed by inheritance but not by composition?).
> As an example if you had a method that outputs XML and you want JSON, you can use an adapter object to call it, read the string, and reformat and pass it through. That works (and I've done things just as bad or worse many times from necessity), but it's definitely more hackish and less efficient than a solution that uses object fields to output JSON natively. Inheritance is my familiar way to do this in cases where I don't control the code (in which case injecting a Writer class of some sort probably makes more sense).
You can do this just as easily without inheritance. You just need a way to access the member fields--whether you access them via inheritance or otherwise doesn't really matter. E.g.,
class Foo:
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y
def xml(self) -> str:
return f"<foo x={self.x} y={self.y} />"
# Via inheritance
class JSONFoo(Foo):
def json(self) -> str:
return json.dumps({"x": self.x, "y": self.y})
# Via composition
class JSONFoo:
def __init__(self, foo: Foo) -> None:
self.foo = foo
def xml(self) -> str:
return self.foo.xml()
def json(self) -> str:
return json.dumps({"x": self.foo.x, "y": self.foo.y})
# Simple
def json_foo(foo: Foo) -> str:
return json.dumps({"x": foo.x, "y": foo.y})
> I'm not familiar with Go but apparently it has "embedding" which is a little different. Maybe that works. But it's largely playing the same role - a way to extend a class you might not control.Go's embedding is exactly composition. It doesn't let you access private member data of the embedded class. It's just syntax sugar for delegation. In other words, you could create the composition version of the JSONFoo class like this:
type Foo struct { X: int; Y: int }
func (f *Foo) XML() string { return fmt.Sprintf("<foo x=%d y=%d />", f.X, f.Y) }
// The compiler will automatically generate this method:
//
// func (jf *JSONFoo) XML() string { return jf.Foo.XML() }
//
// And for a given instance of JSONFoo, the compiler will convert all instances
// of jsonFooInstance.X to jsonFooInstance.Foo.X.
type JSONFoo struct {Foo}
func (jf *JSONFoo) JSON() string { return fmt.Sprintf(`{"x": %d, "y": %d}`, jf.X, jf.Y) }
Note that a `JSONFoo` isn't a `Foo`. This is an error: func printXML(foo *Foo) { fmt.Println(foo.XML()) }
func main() {
jsonFooInstance := JSONFoo{0, 0}
// printXML(jsonFooInstance) // error
printXML(jsonFooInstance.Foo) // correct!
}I tend to lean towards inheritance for functional extensions and composition for everything else, but there are so many ways to skin an OO cat.
If that's what people think OOP is supposed to look like, no wonder they don't like it.
I've yet to find a non boring, yet descriptive, consensual alternate definition of OOP; the first thought people have is more Java than Smalltalk, and if you exclude inheritance and everything virtual, then it boils down to syntactic sugar for single non-dynamic dispatch, and encapsulation. On the other (but still very sweet) hand, is Ruby OOP when you write 10.times { puts "hello" }? I don't know. Or rather: it's completely arbitrary. Even what is the most useful, when used cleanly is not exclusive of "OOP": invariants are also most important, and arguably way more well known by practitioners, in FP.
Natural language is inherently descriptive. If there is a better "OOP", you just have to fight to make it prevail, so that people think of it when they hear "OOP". Maybe we can make the bad teachers stop in other ways too. Like not even using the word, and making "alternate" approaches (maybe even what you are calling OOP) fashionable.
My problem with the example is that Book should not have a sell() method. Instead, BookStore should have a sell(Book) method. The machinery for how to send in a credit card transaction does not belong in Book. You still need it - it has to live somewhere - but BookStore is the place, not Book.
Ugh.
Polymorphism is far from unique to OO, and predates it.
If by encapsulation you mean information hiding, that's as old as the birth of OO. I've also sometimes encountered an alternate definition of encapsulation that more or less means "information hiding in OO", which seems a bit circular to me?
That doesn't mean OO can't have those features, but you didn't say "OOP usually includes loops and conditionals", because it's too obvious to mention.
You even know what the problems are with the design - you described them quite well. But it's not showing that OOP is horribly flawed. It's just showing you that you need a different design.
Off the top of my head you could survey the landscape of OOP books, blog posts, and code bases throughout history. But it doesn’t really matter—even if you don’t believe me and all OOP programmers believe these are design flaws, then my question remains: what distinguishes “good OO design” from data oriented design?
If you're having a hierarchy just to have a hierarchy, that's about as wise as having gotos just to have gotos. Nobody sane would do that today; maybe we'll get there with hierarchy, too.
I'm not sure what your definition of "data-oriented design" is, so it's hard for me to say how OO is different. I'll take a stab at it anyway, but know in advance that my response may be orthogonal to your question.
You've got data - say, data about a book, in the original example. In the structured programming days, that would be an "entity"; now it's an "object". The difference is that, with private methods, nobody can modify the book's data just by having a reference or a pointer to it. (You could kind of do this in C with a source file that would operate on the structure, and some of the methods being file static. But that doesn't keep any other code that has a .h file from modifying the structure without using the functions.)
The philosophical difference with OO might be that OO uses the compiler to enforce that data is only accessed in approved ways. This is a logical extension of static typing. (Of course, for non-static-typed OO languages, it doesn't happen that way. They enforce it at runtime.)
Or is OO encapsulation + inheritance? One problem is that the OOP community seems split about whether or not inheritance is a defining feature of OOP. And since encapsulation isn't a defining feature, surely it's inheritance. Unfortunately, there's no clear consensus, so OO as a term is not very useful.
> The philosophical difference with OO might be that OO uses the compiler to enforce that data is only accessed in approved ways. This is a logical extension of static typing. (Of course, for non-static-typed OO languages, it doesn't happen that way. They enforce it at runtime.)
This is an interesting distinction; ironically C compilers support encapsulation via opaque pointers while Python doesn't enforce encapsulation even at runtime (it's all based on convention--"consenting adults" and all that).
> I didn't say that inheritance hierarchies are bad. They're great... if you've got entities that are actually in a hierarchy.
I think there are cases where inheritance doesn't overtly bite you, but I've never seen a case where inheritance is actually cleaner than composition (with the exception that in many languages, inheritance is the only way to automatically delegate--they lack something like Go's struct embedding). Specifically in your Statement example, you could have gotten your polymorphism from a Statement interface instead of a base class.
Inheritance seems like just a particular subset of composition and polymorphism--there are cases for which it is appropriate, but why does it need to exist at all when you can get the same benefits from composing the constituent components (components being polymorphism and composition via interfaces/first-class-functions and object-/functional-composition, respectively)? It feels like having an add4() function--it's not always bad (sometimes you really need to add 4 to another number), but the more general 2-argument add() function works just as well in the cases where add4() doesn't overtly bite you, and it's much less likely to be abused.
Classic, pre-object Pascal. C. Very little new in the last few decades, because OOP is a very influential paradigm.
I just think more about hoe best to structure my code. Sometimes a OOP-like pattern works well, sometimes something else works better.
Yes this is correct. But data abstraction and encapsulation are found in all object-based code; they're not specific or related to OOP.
That is, it is more about the "how" than the "what".
This lets you isolate where your contract/invariant violations are more easily. Once addressed in the member functions, either it fixes it in the callers, or their use is now invalid and becomes a compile or runtime error.
The difficulty with that advice is that it doesn't become applicable until you've gotten maybe 5 years worth of humble pies thrown in your face, for moments where you realize in retrospect that you made the wrong call.
Until then you don't have enough of a body of experience to tell two very different scenarios apart: "things are this way because my predecessors already learned the hard lessons" vs. "things are this way because my predecessors didn't know better."
Paradigms are really useful in practice as a shortcut to implementing 80% of the wisdom until you begin to learn the remaining 20%, and one of the roles of senior engineers is to make sure junior engineers don't mistake the 80% for the 20%.
This is just Chesterton's Fence again, and has the same refutation: if you're putting up a fence that serves some long-term purpose, it is your responsibility to put signs on it explaining what that purpose is. Otherwise people are right to assume it's one of the overwhelming majority of fences that do not, in fact, serve any purpose beyond obstructing their movement.
However, when you write the same architecture over and over, you don't write an explanation each time - for various reasons, a not insignificant one being that there's no obvious place to put it. (I use dependency injection in my code. Where should I explain why - on each class? In a separate file in the project? In the documentation? Should I write documentation, then? What if I write it and others continue to "new" services inside their code?)
Your approach absolves yourself of exercising agency to discern purpose in fences that lack signage in a real world where plenty of purposeful fences lack signage. Just because it’s someone else’s responsibility doesn’t mean they actually do it, and just because they didn’t put a sign there doesn’t mean you’re blameless if you cause a service outage.
I work with cryptography and it would take a book to put adequate signage on some fences. Nobody’s going to write why they’re using AES GCM every time they use AES in that code.
Global variables are called "Singletons". DI injects a reference to all the globals you need to do your work, so you don't feel so grubby. Often DI injects some kind of accessor to the database, a giant store of global variables.
Procedures are called beans, but you can tell they're procedures because they have a single public entry point and are usually called SomeKindOfVerber with a main verb() or do() or execute() method, they have the procedures they call (sorry, I mean "collaborators") injected, and they themselves are injected to where they'll be called from. Their state is ephemeral, and only exists for the purpose of executing a single flow of logic (this is called "bean scope").
All you need to do to describe your code as object-oriented is convert your procedures into beans, and tada!
I also agree that is kinda of funny to constantly so the Verber classes everywhere in Java, but it has at least improved over the last several years with the standardization around Function<T,R>, Supplier<T>, Consumer<T>, etc. Lambda syntax and method references also help. I'd admit the syntax for using a functional style in Java is still a little jenky, but its quite an improvement from where things were back in the day.
It is very hard to do something that is 'good' in technical terms while also being 'good' in product terms. While the discussion often doesn't revolve around the practical implications of taking steps toward a commercially uncommon language or paradigm during business engineering it is pretty much where all the 'stuff gets done'.
And java isn’t where all the ‘stuff gets done’ - well, outside of the enterprise programming bubble. JavaScript and (surprisingly) python are the workhorses of the modern startup ecosystem. I can’t speak with certainty about python, but separating logic and data, and using immutability with stateless components and functions is all very common in the JavaScript world.
C, C++, Java, Python, JavaScript, C#, even PHP are not exactly ideal, even in their special niches, but a lot of real world stuff, if not most 'stuff' gets done using that.
The biggest upside I see for automated DI is to get back concision once you've parameterized your code by the code it depends on. Usually this parameterization is done for unit testing purposes.
Direct calls are hard to exclude or intercept for testing, so you call a method on an interface or a base class or an object instead, and now you need to be initialized with a reference which may be replaced for testing purposes. But the requirement for initialization remains in production, even when there is normally exactly one candidate and it might not even require state if it wasn't for all the other references it in turn needs to call.
The great increase in the burden of initialization creates demand for the IoC / DI container. And once you have a container framework which takes care of gluing objects together, why not let it stick transparent adapters (proxies) around the objects too? Proxies for per-request state, multi-threading, transaction management, caching, all that good Common Lisp advice of before, after and around.
The temptation for clever people can be hard to resist, and before you know it your local code has global side effects unknowable to people who don't have the full picture in their head.
I actually blame unit testing, particularly when combined with a static type system. Static binding, in particular: you could mock static calls if dynamic binding like Emacs Lisp was available!
Unit testing is great at the leaves of the call graph, for self-contained things like container libraries, or things which behave functionally, i.e. they transform things.
Unit testing is like arthritis in the middle of the call graph. It makes rearchitecting much harder by baking in assumptions both upstream and downstream, and the easy temptation of mocking usually overspecifies the interaction with collaborators. The parameterization demands DI which encourages too much cleverness, as discussed. And unit tests don't leave you very confident that things will hold together because test mocks and stubs don't necessarily replicate the behaviour of the real things.
I like integration testing higher up the stack, and trying to convert a program as much as possible into a composition of functional elements - things that belong on or near the leaves of the call graph, and leaving as little "middle" as possible to be infected with unit tests.
Yep, you get all the downsides of coding in Basic with the bonus of the performance hit you take from all the reflective instantiation.
They can help simplify what is essentially a problem of complexity that is self-induced by the original decision to use OOP. But it's rare that they don't also introduce tons of additional other complexity, e.g., at runtime. Debugging these applications is one of the most infuriating things I've ever done as a programmer.
Secondly, you don't even need to use interfaces with one-off implementations if you don't want. You can inject your concrete implementation class instances directly. Using interfaces just makes it easier to swap implementations as needed. The unit testability argument is completely orthogonal (and also in the case of the JVM, you can mock classes as they're open by default, so again, you don't even need to use one-off interfaces).
And I don't see how this issue is limited to OOP. At the end of the day, you're going to have to hook up your components and object graph anyway, either manually or using some kind of DI, so the complexity is always there. Even if you don't write interfaces with single implementors, non-trivial programs quickly become extremely verbose to implement. Maybe something like Scala's `implicit` can be of help here.
> Debugging these applications is one of the most infuriating things I've ever done as a programmer.
Again, Dagger and Micronaut do DI at compile time using codegen, so nothing to debug at runtime.
Non-trivial programs don't have to be verbose. They are generally made so by the addition of unnecessary "features" that, in turn, require even more additions "to reduce complexity."
but the linux kernel is definitely OO, except vtables are filled by hand instead than with the help of a compiler.
https://www.kernel.org/doc/html/v4.10/driver-api/infrastruct...
structs with data + function pointers are literally OOP objects. see e.g. https://lwn.net/Articles/444910/
> you'll see that there are object oriented concepts like information hiding and polymorphism all over the place; they just don't formalize them with the "class" or "private" keywords.
yes, that's still OO. python doesn't have a private keyword either and that does not make its classes not OO.
You are misattributing that to OO programming.
The original edition of Code Complete, written in 1993, makes absolutely no mention of OO programming or any OO principles. However it has a very good discussion of information hiding, and procedural code written in the various styles that it recommends do not create a lot of global state. (And yes, I have worked with such code.)
One of the best things about OO was that it pushed programmers who had not absorbed best practices towards that. However OO encourages combining the principle of information hiding with OO notions of inheritance. Many learned the hard way to prefer composition over inheritance. (Many, unfortunately, never learned that. Just as many procedural programmers never learned about information hiding.)
When you do build programs like this, code gets more and more rigid until it becomes a maintenance nightmare, and extremely expensive to pivot functionality. Sometimes having a complex object hierarchy literally stops you making some new feature.
A lot of mental effort is sucked up in mashing problem spaces into hierarchies, and once there the program is constrained by them.
Implementation inheritance is often justified as a way of "reusing" code, but as it turns out, we've merely introduced undesired coupling instead of the seamless reuse we might have expected.
In principle, with a sufficiently-robust type system, the invariants expected by the base class would be encoded in the typing, and the derived class would only be able to narrow them.
(Of course, OOP languages tend to either be untyped or have insufficiently robust type systems for this; robust type systems are more frequently associated with functional languages.)
why would you make internal methods virtual though?
So, for example, Java's Map is an interface type with, for example, .get() and .put() methods; TreeMap and HashMap implement those methods differently. There are similar things in Smalltalk-80 and the C++ STL.
So clearly you think Java, Smalltalk, and C++ are "a bass-ackward parody of proper OO design." I'm interested to hear what systems you consider paragons of proper OO design.
You might find the book Program Development in Java: Abstraction, Specification, and Object-Oriented Design by Barbara Liskov and John Guttag very useful for understanding OOD/OOP.
You've touched on a whole host of performance issues here too. Not only are we making indirection after indirection when accessing virtual methods and data (my poor cache), but invariably these are all stored in heap memory, so we get the worst possible performance profile baked in.
> the actual code that gets executed depends on whether the object is one of a "derived" class, where that method might have been changed in ways that might break any amount of expected invariants.
On top of that inheritance makes the concrete type unknowable at compile time just to add to the pain.
> Implementation inheritance is often justified as a way of "reusing" code, but as it turns out, we've merely introduced undesired coupling instead of the seamless reuse we might have expected.
Yes this 100%. Inheritance actually stands in the way of code reuse. If I have a class that is kind of similar to another, but different enough to "require" being another class, oh dear they're now incompatible. So you try to kind of fit it into a subclass, but it makes more sense in another, and now you're spending mental effort on a problem created by architecture and not the actual thing you're trying to solve.
This is really clear in the game development world, where game objects often just don't fit into the inheritance format, even with multiple inheritance. This is why most complex games are built with the entity component system pattern as it focuses entirely on composition.
Perhaps the crux of the issue is that inheritance encourages design by "identity" (mental concept) rather than "attribute" (data). Thus to solve problems we must mentally classify them into some taxonomy as if they are animal species before we even write any code. This is just extra work in exchange for all the issues mentioned.
It's not supposed to, and if it does, the design is clearly broken. Now the (rhetorical) question is: does it happens often. The not so rhetorical question is: is it possible to make it happen rarely. The even more interesting question is: is it easy / more practical compared to alternative approaches, and if not then what is the point.
My opinion on SOLID is that there is precisely one hard "principle", and it is worthwhile: the LSP (it derives directly from logic, that's why). I believe that Open-close is even borderline insane, and that if there is a crazy way to make it not insane, this way is probably applied by so few people that it may well be irrelevant -- most people will try to apply it in ways that will quickly put them at risk for LSP violation, but LSP is far more important (or they will try to apply it in bad places, but that is another story). Plus programs designed with complex hierarchies are often missing the point of execution contexts, and then understanding them is absolute hell -- their original authors sometimes do not understand them themselves. (The rest of SOLID are soft attempts at fixing self-inflicted wounds, sometimes even reasonable if you insist on doing Javaesque / old-C++-esque OOO, but I digress.)
I'm not sure why anybody thought that kind of OOO was a good idea, or that the main characteristic in interesting big programs was the usage of "classes". I even find the suggestion of causation instead of mere correlation dubious; they were already quite a good number of big programs before, and what permitted the explosion of program size was more the ever growing capabilities of computers, that happened, in affordable versions, during the Java-like OOO hype. Besides the simplistic reductionism (we can start with: which classes model entities, which classes model values, which classes are controllers, etc.) which is not too much a big deal in practice, modeling with class diagrams is often missing the river in the middle of the forest for some groups of intertwined trees.
The Smalltalk-80 container hierarchy demonstrates that you can get a lot of mileage out of simple single-dispatch virtual methods with inheritance. The Smalltalk-78 system, which you can try at https://lively-web.org/users/bert/Smalltalk-78.html (although it's not working for me at the moment), got a multiwindow GUI with an IDE running usably on an Intel 8086 with 256KiB of RAM, in only about 100 classes and 2000 methods totaling 200 KiB of code. This is not what I would describe as a "big program", but it is a fairly impressive program nonetheless. Seeing that kind of thing is what led people to adopt object-oriented programming.
I firmly agree.
I wrote https://www.perlmonks.org/?node_id=318257 over 15 years ago. I still agree with the fundamental criticism of "OO everywhere" that I offered there.
Though w.r.t. the speculation in your final "Disclaimer", I'd expect the algebraically inclined to prefer either the elegant combinatorical explosion of a J, or the build-it-yourself language of a metaprogrammed Lisp.
I find OOP useful for configuration and library-level APIs (e.g. for injecting dependencies and wrapping things), when there is really zero expectation that a user should have to understand the class definition entirely. But that's about it.
Where it seems to break down for me is when classes are actively used at the application implementation level for general encapsulation. E.g. data access layers. This can be done perfectly fine with modules/packages. Adding inheritance is just asking for trouble in a team setting IMHO.
Trouble comes when there is no clear peer review and style policy to avoid classes for anything other than config or distributing libraries (as separate projects). What ends up happening is a proliferation of subclasses or method overrides when developers are in a rush to ship features without understanding the whole codebase. This is a technical loan with very high interest.
It makes sense rationally in the moment, as classes have an inviting feel to the user as a kind of grab bag of related functionalities that are easily introspected (at first). Compare it to searching through docs for all the different namespaces in a package and learning what types their various functions support, it requires more thought and grasping the concepts of the library. Alternatively, a class with a broadly defined purpose starts to look like a common utility to throw things at. It's a grab bag of stuff that is more amenable to hacking with blinders on, in a way.
Next thing you know the chains of inheritance and method overrides have grown into a very hard to disentangle hydra. The accumulated overhead of maintaining it is compounded by the debugging challenge of knowing where exactly a given instance is coming from and which layers it has been through on its way to a given breakpoint.
The mental effort of building up taxonomies in particular is exactly what put me off inheritance (and fussy type systems in general).
It's too easy to jump down that rabbit hole without considering whether it's a productive activity.
Inheritance is used inappropriately (outside of frameworks) almost everywhere.
When you implement an extended jump table (a table set that extends an existing table def) in C, that is also just inheritance.
There is a good reason the first C++ compiler was just implemented as a bunch of macros in C.
I wrote a blog post earlier this year based on this paper, describing how using the information hiding criterion naturally leads to an improved system structure when compared to using a procedural criterion [1]
I'm an advocate for OOP because it greatly favors information hiding and encapsulation, helping to manage the ever-growing complexity of software projects, making "it possible to develop much larger programs than before, maybe 10x larger" (quoted from the linked article)
[1] https://thomasvilhena.com/2020/03/a-strategy-for-effective-s...
If OOP is so abstract as to be indistinguishable from other paradigms, then what value does it add?
in case anyone's unfamiliar, here's one basic way to emulate objects with closures. i think i saw it in SICP (?), reproducing it here because i just love how simple it is:
// might be easier to first look at
// the usage example at the bottom
let Point = (x, y) => {
let self = (message, args=null) => {
switch (message) {
// getters
case 'x': return x;
case 'y': return y;
// some operations
// (immutable, but that's not required)
case 'toString':
return `Point(x=${x}, y=${y})`;
case 'move':
let [dx, dy] = args;
// use our getters
return Point(self('x')+dx, self('y')+dy);
// let's get DRY!
case 'plus':
let [other] = args;
return self('move', other('x'), other('y'));
default:
throw Error(`unknown message: ${message} ${JSON.stringify(args)}`);
}
};
return self;
};
let p1 = Point(3, 5);
// sending messages
p1('x') === 3;
p1('y') === 5;
p1('move', [1, 2])('toString');
// --> "Point(x=4, y=7)"
let p2 = Point(1, 2);
p1('plus', [p2])('toString');
// --> "Point(x=4, y=7)"
dynamic dispatch & message passing, just like that! and you can easily do `__getattr__/method_missing`-style dynamicism just by looking at `message`.for the other way around, see how Java lambdas desugar to objects with a "call" method and closed-over variables as members.
i remember reading this somewhere: "branching on values is the ultimate dynamic dispatch" :)
Dynamic dispatch implies that you dispatch based on the class of the receiver. For instance, you can't have a Point3 value that uses Point's implementation of 'x' and its own implementation of 'toString' under this example.
let Point3 = (x, y, z) => {
let parent = Point(x, y);
let self = (message, args=null) => {
switch (message) {
// getters
case 'z': return z
// some operations
// (immutable, but that's not required)
case 'toString':
return `Point(x=${x}, y=${y}, z=${z})`;
case 'move':
let [dx, dy, dz] = args;
// use our getters
return Point(self('x')+dx, self('y')+dy, self('z')+dz);
// let's get DRY!
case 'plus':
let [other] = args;
return self('move', other('x'), other('y'), other('z'));
default:
parent(msg, args)
}
};
return self;
};> you can't have a Point3 value that uses Point's implementation of 'x' and its own implementation of 'toString' under this example.
you can't do much of anything under this this example! i didn't think a whole object system would fit in a HN comment, i intended it to be minimal :)
let moveUp = ['move', [0, 1]];
p1(...moveUp)
p2(...moveUp)
but you can't get a `move` that's "bound" to a particular object¹ – the "receiver" is decided when you "send" the message. which reminds me of how JS methods work: `this` is bound to what's "to the left of the dot" when you call a method, so unlike Python, obj.foo(1)
is not the same as let f = obj.foo
f(1) // Error: 'this' is undefined
maybe there's a connection to some language that inspired JS' OO model, with prototypes and all that?---
¹ well, unless you explicitly wrap it in another closure like
(args) => p1('move', args)Edit: To answer your question, the set of all public methods could be seen as 1, but not the only, interpretation of the functional interface.
> the set of all public methods could be seen as 1, but not the only, interpretation of the functional interface.
could you describe another interpretation? "the set of public methods"¹ is the only thing i can think of when i hear about an object's "functional interface". it could be a terminology issue though – i'm reading "functional interface" in a general sort of way, but i could imagine it having some specific definition in OO theory.
---
¹ or recognized messages, in a more smalltalk-ish context
PS. if you're interested in weird perspectives on this, you might find Coinduction (and "codata") interesting. from wikipedia:
> Informally, rather than defining a function by pattern-matching on each of the inductive constructors, one defines each of the "destructors" or "observers" over the function result.
it's a deep rabbit hole, but i remember it being applied OOP-ish things – "destructors" would roughly correspond to exposed methods.
Objects are a poor man's closures. And closures are a poor man's objects.
Modern languages have both. And they serve different purposes.
though i must say, i'm pretty happy in languages that have closures but don't have objects, as long as there's a nice way to do ad-hoc polymorphism (like traits/typeclasses)
let pair = (a, b) => (
(ix) => (
ix === 0 ? a :
ix === 1 ? b :
undefined
)
);
let p = pair(3, "foo");
p(0) // 3
p(1) // "foo"
i've seen sth like this used as the definition of tuples in a math context. (there are also other ways to do it too, like church/scott encoding)however the object has the extra wrinkle of self-referentiality, because the `self` function is defined recursively
Usually this is a hash (dictionary) which is similar to the object with fields that we all know, but it can just as easily be an array or scalar value.
From Alan Kay (who coined the term)[1]:
> OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things. It can be done in Smalltalk and in LISP. There are possibly other systems in which this is possible, but I'm not aware of them.
Almost anyone who has dealt with present-day OOP (Python, Ruby, C++, C#, others, or, God forbid, Java) has had the same thoughts as you, I'm guessing. I've come to the conclusion that OO as originally envisioned had a lot of good ideas, and these are surprisingly compatible with and complementary to a lot of the (almost-)pure FP ideas that have gained traction over the last few years.
OOP also had huge mistakes:
- Class inheritance is the biggest one. Kay himself mentions inheritance appearing only in the later versions of Xerox's Smalltalk. No need to really elaborate. Implement interfaces, compose behavior and don't inherit. He has posted some more of his thoughts on this exact topic. [2] - Java was a giant industry-harming one. Now multiple generations of programmers have been brought up to think that the definition of an object is to write a class (which is not an essential characteristic), declare internal state and then write accessors and mutators for every one of them. We might as well write COBOL. But, having tried to solve problems in Java, I get why it's done. The OO is so weak and awful that I'd rather just crank out slightly-safer C in Java than jump through the horrific hoops. (Just as an aside, I find it sad that Sun had the teams that developed both Java and Self. Self used prototypes instead of classes, which influenced Javascript, but also had a killer JIT that made it super fast and a ground-breaking generational garbage collector. Sun ended up cannibalizing the Self team to assist with Java, and some of the features of the JVM were ports of work that had originally been done for the Self VM.) - C++ was kind of a mistake, because it introduced the interminable "public" and "private" and now the endless litany of stupid access modifiers. They were needed because C++ had this need to be compatible with C and do dynamic dispatch and have some semblance of type safety (though C is only weakly typed) and have it run (and compile) before the heat death of the universe. C++ isn't as horrible as what came after it though IMO (i.e. Java). - Xerox horribly mismanaging Smalltalk was another mistake. Xerox was smart enough to realize that Smalltalk was insanely valuable, but instead of realizing that it should be a loss leader/free drugs, they decided to lock it away in the high tower and then let everyone else pillage their research that could be commercialized (GUIs on the desktop, laser printers, Ethernet, among many others). This literally led Apple to develop Objective-C.
I really went down a rabbit hole on this topic around two years ago. Two ideas really helped crystalize the idea for me, both from Kay.
One is that the Internet is an object-oriented system. It communicates via message-passing, it is fault-tolerant, and all of its behavior is defined at runtime (i.e. dynamically bound). We don't need to bring the entire Internet down when someone develops a new network protocol or when a gateway dies or some other problem happens. It continues to work and has in fact never gone down (though early on during ARPANet, there were instances of synchronous upgrades [3]).
The other, deeper one (and the much more open one) is that "data" is error-prone and should be avoided and is, in fact, "the problem" (i.e. the root of the Software Crisis). This sounds preposterous to most programmers.[4] In fact, I didn't even get it when I first heard it. I think the basic idea is that he uses "data" as a physicist or statistician would use it: quantities measured in the world with varying accuracy/precision. This seemed preposterous or even heretical until I started noticing it in the large codebase I was working on at the time. A lot of the code was in a nominally object-oriented language, but the style was very procedural (i.e. "If a then do X, do Y; else do Z"). I noticed that all of our code was working on large data structures (think a relational database schema), but there was no coherent description of it or what it meant anywhere. If I touched one of the pieces of code, I invariably had to start adding more conditional branches in order to preserve correctness. In order to make any sense of the code, I made a ton of implicit assumptions about what the patterns I saw actually meant, rather than relying on the program itself to communicate this through its code or meaningful constraints on the data. We would be better served with "information" with less noise and more signal than what we get with data and data structures (think Shannon). The only real success we've had with data is a few encoding schemes (e.g. ASCII/Unicode, 8-bit bytes, IEEE-754, etc.), but that approach clearly doesn't scale beyond a dozen or two data encoding schemes. OO (of the high signal-to-noise variety championed by Kay) at least has a story for this, which is that certain patterns do indeed have predictable characteristics, and complex patterns can usually be composed by a handful of simple ones. I have yet to see a system like this actually in practice, but I can imagine something like it could exist, given the appropriate tools (in particular, a good, state-of-the-art language that isn't some rehash of stuff from the 60s - Erlang might fit the bill).
The ideas of DO as articulated by Mike Acton are antithetical to the ideas of OO (although a well-designed future OO system - including late bound hardware - could probably represent and implement a lot of the techniques that Acton talks about to improve performance and quality). This doesn't mean they're wrong, but I see treating data as the _only_ thing that matters as being fundamentally opposed to the idea that data is actually the root of most evil. People adopting DO should be aware that robustly applying the technique in the long run will require having detailed knowledge of every aspect of the system on every change. (I think it's not a coincidence that it was originally developed for game dev, which often doesn't have the same long-term maintenance requirements as other kinds of software, and certainly not software like the Internet.)
FP and OO are more complementary. Immutability and referential transparency can be very valuable in object systems; encapsulation, extreme late binding and message passing can be very valuable in functional systems (in fact, Haskell and Erlang surpass "OO" languages when scored on some of these characteristics). Scala explicitly embraces the complementarity between the two paradigms.
[1] https://userpage.fu-berlin.de/~ram/pub/pub_jf47ht81Ht/doc_ka...
[2] https://www.quora.com/What-does-Alan-Kay-think-about-inherit...
- Class inheritance is the biggest one. Kay himself mentions inheritance appearing only in the later versions of Xerox's Smalltalk. No need to really elaborate. Implement interfaces, compose behavior and don't inherit. He has posted some more of his thoughts on this exact topic.[2]
- Java was a giant industry-harming one. Now multiple generations of programmers have been brought up to think that the definition of an object is to write a class (which is not an essential characteristic), declare internal state and then write accessors and mutators for every one of them. We might as well write COBOL. But, having tried to solve problems in Java, I get why it's done. The OO is so weak and awful that I'd rather just crank out slightly-safer C in Java than jump through the horrific hoops. (Just as an aside, I find it sad that Sun had the teams that developed both Java and Self. Self used prototypes instead of classes, which influenced Javascript, but also had a killer JIT that made it super fast and a ground-breaking generational garbage collector. Sun ended up cannibalizing the Self team to assist with Java, and some of the features of the JVM were ports of work that had originally been done for the Self VM.)
- C++ was kind of a mistake, because it introduced the interminable "public" and "private" and now the endless litany of stupid access modifiers. They were needed because C++ had this need to be compatible with C and do dynamic dispatch and have some semblance of type safety (though C is only weakly typed) and have it run (and compile) before the heat death of the universe. C++ isn't as horrible as what came after it though IMO (i.e. Java).
- Xerox horribly mismanaging Smalltalk was another mistake. Xerox was smart enough to realize that Smalltalk was insanely valuable, but instead of realizing that it should be a loss leader/free drugs, they decided to lock it away in the high tower and then let everyone else pillage their research that could be commercialized (GUIs on the desktop, laser printers, Ethernet, among many others). This literally led Apple to develop Objective-C.
Having read "Software Fundamentals" [1], I think David Parnas [2] would beg to differ on that bit. Not the least because he actually invented the concept.
Parnas wrote extensively on information hiding as the basis for modular program construction, abstract interfaces that provide services without revealing implementation, and other software engineering topics.
[1] https://www.goodreads.com/book/show/1416932.Software_Fundame...
Whether or not the original quote meant to imply that I’m not sure, but do see how it could be read that way.
In short, information hiding is a concept from modular procedural programming that was then adopted by OOP, not an original OOP concept.
In general, OOP is really mostly a case of "let's take a bunch of known best practices and encourage (or even enforce) them on the language level". Procedural programming can be as object-oriented (or not) as you like. With minimal amounts of syntactic sugar in the compiler, like e.g. rewriting "foo.bar(...)" to "bar(foo,...)" you won't even be able to easily tell the difference when you see the source code.
While I'm at it, the critique that procedural programming necessarily results in a quagmire of global state is also wrong. It's all up to the programmer. Nobody forces you to use globals. Some OO languages try to force you not to, but usually fail in the face of determined opposition from the coder.
In essence, foo coders will find a way to produce foo code.
My mind was blown when I first saw it. https://www.destroyallsoftware.com/screencasts/catalog/funct...
http://www.smashcompany.com/technology/object-oriented-progr...
https://physics.stackexchange.com/questions/29175/why-is-inf...
That's not what I'm seeing. The usual discourse around OOP is that some parts of it are bad. In particular, overriding concrete methods, and in general Java-like inheritance brings a lot of pain. "Everything is an object" from C# and Java also gets a lot of well-deserved criticism (https://steve-yegge.blogspot.com/2006/03/execution-in-kingdo...).
Other parts, like polymorphism, incapsulation, binding data with behaviour - I don't see that much criticism against this. For me, modern languages like Go or Rust (yes, I know it's almost a meme at this point, but still) bring sane OOP approach: while not being strictly OOP, they take the parts that work, and remove the parts that don't, while at the same time adopting many practices and abstractions from functional languages that proved useful.
It’s all just a bunch of functions and memory
Towards the end of the talk note of the stickers on his laptop
* Data Oriented Programming. There is data, and there are functions operating on the data.
* Functional Programming. Variant of DOP, with a strong emphasis on purism and abstract algebra concepts.
* Procedural Programming. Variant of DOP, lacking language level mechanisms or best practices around polymorphic interfaces. Largely obsolete.
* Object Oriented Programming. Everything is an object.
Information hiding and polymorphism are readily available in all styles, except PP. The trouble with OOP is that insists on two big lies:
* Everything is an object, therefore data is an object, therefore data must be hidden. In fact, data simply 'is' and you can't hide it. Think JSON. There is nothing to hide.
* Everything is a polymorphic object, codified as the popular 'Open/Closed Principle'. In fact, most code is functions that just compute some data. Never needs N different polymorphic variants. If the business cases ever warrant such variants, simply refactor a polymorphic interface into the code base.
To further clarify, one can use DOP style in many languages, including languages that are ostensibly OO. DOP is a style, using data hiding and polymorphism as sparingly as possible, as opposed to OOP style, which strongly encourages using data hiding and polymorphism at every corner.
* Everything is emphatically NOT an object. There is immutable data, there are functions and there are modules. These are not the interchangeable, learn to use them in the proper context.
* Everything is emphatically NOT a message. There are plain functions, there are polymorphic functions, there are RPCs and there are async messages. These are not interchangeable, learn to use them in the proper context.
it seems all programming paradigms end up devolving into global state by friday. because too many developers cannot recognise global state, instead recognising only its syntax
Mutate coarse-grained or global state is pretty much similar to functional programming approaches. The global state is a big tree, and there are functions designed for this type, there are also functions and types for every fraction of the tree. It's straight forward, elegant, and extensible.
It's basically what Alan Perlis said in SICP: "It is better to have 100 functions operate on one data structure than to have 10 functions operate on 10 data structures."
The only difference is just functional programming always transact the whole tree instead of mutating it, since they use immutable algebra data type.
It's not far from what Redux/Erlang process/Haskell ST Monad etc does.
IMO OO makes it more obscure and too subjective compared to this approach. It makes people lost the whole picture very quickly, and then accidental complexity comes in play in every corner.
OOP brought the terrible idea of coupling state and behavior, so of course "global state" would be wrong.
• OOP, narrow sense (emphasis on “object”): data-with-associated-functions, encapsulation, etc.
• OOP, broad sense (emphasis on “oriented”): organizing your program around objects that model the nouns in the problem domain, “everything is an object”.
A (satirical) illustration of the latter is in the first few paragraphs of https://caseymuratori.com/blog_0015 (from 2014), which gives an example where, to write a payroll system, you first start by designing classes for “Employee”, “Manager”, etc.
A (non-satirical) illustration is in the infamous “TDD Sudoku” series of blog posts (follow the links from http://ravimohan.blogspot.com/2007/04/learning-from-sudoku-s... or https://news.ycombinator.com/item?id=3033446) where (in contrast to Norvig's program which just solves the problem) the TDD/OOP proponent ends up with a “class Game”, “class Grid”, “class Cell”, “class CellGroup” (with derived classes “Row”, “Column”, and “Square”), but ends up nowhere. (From Seibel's post: “…got fixated on the problem of how to represent a Sudoku board. […] basically wandered around for the rest of his five blog postings fiddling with the representation, making it more “object oriented” and then fixing up the tests to work with the new representation and so on until eventually, it seems, he just got bored and gave up, having made only one minor stab at the problem”.)
I think we can agree that this sort of object oriented programming can hurt a lot (thinking about objects is not a substitute for solving your problems, though it is tempting), while objects themselves are useful.
https://web.archive.org/web/20140416021831/http://lists.cano...
(Related)
> The big idea is "messaging" - that is what the kernal of Smalltalk/Squeak is all about (and it's something that was never quite completed in our Xerox PARC phase). The Japanese have a small word - ma - for "that which is in between" - perhaps the nearest English equivalent is "interstitial". The key in making great and growable systems is much more to design how its modules communicate rather than what their internal properties and behaviors should be.
Very relevant: http://slatestarcodex.com/2014/11/03/all-in-all-another-bric...
Two:
100% pure functional programing doesn’t work. Even 98%
pure functional programming doesn’t work
Doesn't work for what?I think pure functional programming has its place, it is not just for everything you want to do. But object oriented programming is also not meant for every domain. It is like saying scalpels are stupid, because you can't build a house with them.
It implies that FP doesn't work in the real world without IO or without a runtime. When the author talks about "the messy parts that interact with the outside world" he's talking about the "Functional Core Imperative Shell" concept [1].
[1] https://www.destroyallsoftware.com/screencasts/catalog/funct...
However that is not entirely true, we can perhaps view analogue computers as purely functional computers, albeit one with continuous functions.
But besides such a weird example, we should not kid ourselves, most imperative languages these days also need a runtime. And if you take that a step further, assembly also needs a runtime. But that is I think not a honest argument, but still something to think about.
If we pursue this further down then we arrive at physical phenomena that do the computations. Are those imperative or functional? But now we are just playing around.
>Doesn't work for what?
Doesn't work for performance, too much copying of immutable state.
If 100% pure means zero mutation, you can't even fit the definition of a program and write output /s
Some of us are just permanently immersed in performance critical stuff. In gamedev, a single millisecond is a significant chunk of my per-frame budget and can stick out like a sore thumb in my profiling flamegraphs as an optimization opportunity / potential cause for hitching.
Pure functional code still has plenty of uses in this environment, but we've also got large chunks of our codebases dedicated to mutating external state (GPUs, Network IO, etc.) in horrifically performance sensitive ways.
Yes, but what is the relative portion of gamedevs among all devs? These people know who they are and they should absolutely obsess over performance. Everyone else should stop and think first.
Okay, okay. I'll make another carveout for certain classes of HPC. But even here, as the joke goes, HPC is the art of taking a CPU-bound task and turning it into an IO-bound task. I have heard of HPC people optimizing their numpy application to really run like the dickens on an HPC cluster, the scale their deploy from 1 to 40,000 nodes, and have the filesystem lock up for hours on what should be a 15 minute task due to the way python takes out r/w transactional locks on each and every one of its import statements.
bigger players have conditioned us to think that the status quo is normal. and this is working pretty well on those who haven't been around for a while.
those who have been around for a while are flabbergasted. dreams of seeing software loading right away and doing things of the past for them hasn't come true. note: things of the past (like loading up a text editor or an image!)
the excitement generated by unreal 5 proves that the status quo is not acceptable. we should be expecting much more.
Yeah. My point is, not that they shouldn't care but WHEN they should care. They should build their system, then measure the bottleneck. I would bet that 4 times out of 5 their initial intuitions are wrong.
And embedded. And anything RTOS. And kernel devs. Video encoders. And even some backend webdev stuff for scaling purpouses. And high performance logging systems. And ...
I'm not in the gaming industry but the software I work on has a lot of visualization and physics simulations, so it's similar. It's definitely a different set of constraints, you deal with abstraction a lot less as well.
I'd highly recommend giving something like game dev a try, even if it's just as a side project. Handmadehero.org has great content. It's a breath of fresh air if you're coming from something like web development.
To maintain 30FPS you must finish a frame in 33ms (1000ms/30). To maintain 60FPS you must finish a frame in 16ms. Some of the VR literature I've read has recommended 90FPS or higher to combat nausea - 11ms. A millisecond could easily be 1/10th of your entire frame budget for the whole game's per-frame processing time, to say nothing of individual subsystems. At least we have multiple cores, but some stuff must run on the main thread...
> Also, rad game tools is a company that makes very useful tools for profiling.
arcin is of course referring to Telemetry[1] here, a wonderful tool for generating explicitly annotated flamegraphs that I've helped integrate into multiple codebases at different jobs. Engine-custom[2] tooling is also common, where you're seeing scopes measure in microseconds. There's a bunch of FOSS alternatives[3] of varying quality / functionality - nothing that's put Telemetry out of business for me yet though. "I'll write one myself for Rust someday!" I keep telling myself ;)
[1] http://www.radgametools.com/telemetry.htm
[2] https://www.youtube.com/watch?v=DMO4X9leTG8
[3] https://github.com/jonasmr/microprofile https://github.com/Celtoys/Remotery https://github.com/loganek/hawktracer https://github.com/google/orbit
>Some of the VR literature I've read has recommended 90FPS or higher to combat nausea - 11ms That explains why VR is so computation heavy. Also curious what the mechanism is why you become nauseous under the 90fps. Perhaps the brain observes you are moving abruptly with lower frame rates, giving a constant sense of acceleration.
About telemetry. It feels weird that they are so flexible about their license, I am come from the corporate world, where licensing is set in stone. Is the game industry mostly informal?
>I'll write one myself for Rust someday You do your game development in Rust? I have interest in Rust, because it has an interesting type system, but perhaps it would be nice to make a simple game in it to learn it and look how that goes.
Thanks for your replies.
It seems to be some form of motion sickness... it seems nobody is 100% sure why that happens either? https://en.wikipedia.org/wiki/Motion_sickness#Pathophysiolog...
I'm lucky enough to not suffer from the effects myself... supposedly Habituation can help, so perhaps I just read enough books in moving vehicles as a child?
> About telemetry. It feels weird that they are so flexible about their license, I am come from the corporate world, where licensing is set in stone. Is the game industry mostly informal?
It varies a good bit. Gamedev middleware shops are often small companies not burdened by large bureaucracies, dealing with high price, low volume sales - so they can afford a more high touch, personalized, and perhaps informal approach to sales. They also want to maximize the amount of devs trying their software - even if they don't use it for their current project or company, they might use it for the next, which is more sales - so they're quite happy to hand out evaluation licenses, or talk about missing features / platforms you might be interested in.
Those licenses are partially enforced by software, and for console versions, they might need to verify your lawyers have executed an appropriate NDA with Sony/Microsoft/Nintendo/??? before they even let you download those versions, however. Possibly via posting to shared, restricted access forums.
More standardized pricing systems are also common.
> You do your game development in Rust? I have interest in Rust, because it has an interesting type system, but perhaps it would be nice to make a simple game in it to learn it and look how that goes.
Just for my side projects so far - my professional gamedev work is generally C++ plus a dozen other languages that vary by company for scripting and supporting tools. There's a lot of interest in Rust, though - it's type system and borrow checker do a lot to catch the kinds of heisenbugs that plague large high-performance C++ codebases (which in turn lead to crunch, missed milestones, etc. depending on the company culture.) On smaller codebases, the borrow checker feels more like a nuisance than a help until you get quite used to it, but at this point I feel like I have gotten used to it (and perhaps become a better programmer in the pursuit).
In my next job search I'll definitely be keeping an eye out for the chance to play with Rust professionally...
Thanks for the resource :) Going to have a look. I am not a web developer, I am very bad at it for whatever reason. I fled the web a long time ago for the big data landscape. I almost ended up creating physical simulations of boats, but I missed that boat somehow, still a bit sad about it.
And of course there is state, I know that every node is plenty full of state, but that is not the point and it is hidden from me.
But if you writing device drivers, sure then it might not be ideal.
Not all tools are useful for everything.
>I think pure functional programming has its place, it is not just for everything you want to do. But object oriented programming is also not meant for every domain. It is like saying scalpels are stupid, because you can't build a house with them.
You answered your own question. "It is not just for everything you want to do." He didn't say it was stupid. He said is is not for everything you want to do. You and the author are in agreement.
However sometimes it is hard or impossible to avoid shared state even in functional approaches. So I suppose that is the 98% part. Things like databases, connection pools, files.
Since I have programmed in Clojure it has made me a better programmer even in OOP languages when I use them. I try to avoid self mutation and shared state as much as possible. I know there is a back and forth on what is the better way to program for a lot of things, but to me avoiding shared state is indisputably better for avoiding surprising bugs, and also for making concurrency much easier.
Also believe functional programming gets one to think in terms of function composition. Even in an OOP-heavy language like Java, one can find themselves developing complex operations as a composition of simpler, reusable operations. While Java may not have proper 1st class function nor solid syntax for working with functional interface, one can still think in terms of composable operations.
That is also my experience when visiting different paradigmas, they make you a better programmer, because they come with a different way of thinking.
>However sometimes it is hard or impossible to avoid shared state even in functional approaches. So I suppose that is the 98% part. Things like databases, connection pools, files.
True, true, most pure languages have an escape hatch. Or they pretend to you model the IO through some clever mathematical contraptions and then run it in the runtime during, you never guess it, runtime. But I feel that is just playing with words. Computers work with mutable state, so the 100% pure functional language doesn't even exist.
> does in general with encapsulated instantiated data in classes being operated on in that class.
I agree, but the general case is always a bit dull. There is no reason for a object oriented programming language to not have immutable objects. Every time you call a method that would normally mutate the object, you get a new one. And then perhaps with something like linear types you can make sure, you only used each object once, so you don't spray needless copies all over the place.
I think the axis he should be talking about is imperative programming and functional programming, those are opposites.
Yes, but those objects would have to be immutable. Much (probably most) OOP and procedural software works with mutable data structures, which are forbidden by pure FP.
There are so many times you don't want to bind functions to the data they operate on, and languages that force OOP always make it painful
The other thing OOP gives you is type taxonomies, but frequently those are an anti-feature. Almost every experienced developer I know thinks inheritance is usually a bad idea.
The OCaml community presents a pretty compelling third option:
OCaml is billed as a 'functional language,' but it doesn't do anything to prevent you from performing mutations or executing side effects anywhere you want. It even has builtin syntax for "for" and "while" loops.
Interestingly, OCaml does afford classes and objects but hardly anyone seems to use them. It's not that OCaml objects are weird or difficult or bad in some way. People just choose to write records and functions. Some of those records have functions in them. Some of the functions mutate state.
In the OCaml world, at least, pretending OOP never happened seems to have worked out just fine.
I disagree. I dont think that is what the author was saying at all, not even a little bit: "100% pure functional programing doesn’t work. Even 98% pure functional programming doesn’t work. But if the slider between functional purity and 1980s BASIC-style imperative messiness is kicked down a few notches — say to 85% — then it really does work. ... It’s possible, and a good idea, to develop large parts of a system in purely functional code. But someone has to write the messy parts that interact with the outside world."
In order to bridge the gap, you have to make some pretty terrible assumptions:
If your code is not OO, it must be FP.
If your code is FP, it must be pure. Therefore,
If your code cannot be pure, it must be OO.
Either I can rely on the fact that `foo() == foo()`, or I can't.
Either I can rely on the fact that `map f . map g == map (f . g)`, or I can't.
Neither OO nor pure FP are required to build high-quality software!
OCaml developers have been living in this world for decades now and they seem to get by just fine. Further, they do this purely by choice! Their language has had OO syntax for decades and they are happy ignoring it!
... you know that the "O" in OCaml stands for object-oriented... right ?
This is how we (try to) write code where I work. Pure algorithms, immutable data types, single point of change. D is pretty good for this, but could be better; ranges allow you to be pretty expressive when composing operations, but immutable types have a lot of problems still.
It actually combines well with OOP. We tend to mostly use classes to group methods and express domain dependency, rather than manage a tiny state subset. So in maybe half to two thirds of our classes, all the fields are set once on startup and then never changed again.
Teams were writing million-lines-of-code Fortran programs in the 1980s to simulate the atmosphere, nuclear weapons, etc. Somehow I think these remarks need some qualification.
> And while I don’t often write classes, I do often use classes that come from libraries. Sometimes these objects seem like they’d be better off as bare functions, but I imagine the same libraries would be harder to use if no functions were wrapped in objects.
I noticed this soon after I got involved with Java. Programmers were using classes where I would use methods. They would have objects which did things where I would have passive objects which have things done to them by methods. A specific example might a Parser class with a parse method, where I would define an entirely passive Grammar object with a parse method, and of course a Tokenizer object where I would use a tokenize method, calling an InputStreamReader class (instead of just InputStream) with a read method. I concluded that any class name ending in "er" was almost invariably unnecessary overhead.
I wondered where this came from. All I could think was in object orientation's roots in simulation, where it's entirely appropriate to have objects which do things, and it was copied from there.
Like the author says, OOP is an organization tool more than anything and like "strategy," "adapter," "Command," "builder," etc signal specific things to your colleagues that IMPROVE understanding and time-to-grok.
I've been working on refactoring some code, and I really like the functional style so originally wrote most of the logic in discrete functions. Multiple functions rely on some similar dependencies so there are some unique parameters for the function and then a handful of parameters that are the same for all functions (stuff like a database connection). It seems good that each of these functions can accept those dependencies in the signature, but it's likely that each of those functions is going to need that same dependency for the whole pipeline. At some point it looks to me like a small object that can handle the initial setup and management of those dependencies is helpful. Functional core imperative shell seems kind of like what I'm doing? Others mentioned there is some merit to being able to reduce the state from global to local in some form and OOP may be a useful for that case. Recommendations for other ways to handle these situations would be helpful as it's possible I'm only reaching for an OOP wrapper because that's what I know.
Inheritance is often a space-goat, but it's not even such a big of a problem with OOP. Core ideas behind OOP are misguided:
Abstraction
Encapsulation
Inheritance
Polymorphism
Encapsulation on a such a fine-grained level as each object/class is like a person putting padlocks on each pocket, so that right hand can't grab thing from the left pocket. The granularity of encapsulation in OOP is just sooo impractical. The right granularity for data-hiding are data-stores, layers, APIs, (micro-)services, modules, not each single little bit of data.Abstractions are costly. They can't be a goal in itself. Abstractions should be applied only where they are needed and beneficial, where benefit of adding them outweighs the cost. Similar with Inheritance and Polymorphism - they are abstractions that have a cost. Sometimes worth-it, sometimes not really.
Object itself is a bad idea. Passing around references to objects (data with attached behavior, instead of POD - plain-old-data) forces you to scatter your data across many tiny bits, which makes everything way more complex and slow (poor cache locality, layers and layers of indirection). Instead of passing an Id or bunch of fields, now you're passing data plus some abstractions to manipulate it everywhere. You generally get a graph, and graphs are the most general and thus most difficult to use data-structure out there. Coordination becomes a nightmare really quickly.
OOP is just a busy-work, and trying to pretend that you can just play with abstraction and taxonomies and ignore the fact that effectively your software is supposed to manipulate data to give an expected result, and not be a little god-game of modeling the world. Ignoring that there are some ways to structure your data that supports well what you're trying to do, and a graph of abstract object is very rarely the best choice.
And so on... https://dpc.pw/the-faster-you-unlearn-oop-the-better-for-you...
For a while I've assumed that OOP as in C++/Java essentially formalized modular programming in C. In other words, that people were already writing programs whose state was divided into functional areas, with some functions serving as the interfaces between the modules. With a class-based system you can rigidly formalize this; and then OOP as we use the term essentially just reinterprets this formalization as actually creating the architectural paradigm that had already evolved as programs grew.
(This is NOT meant as the one way to sum up the whole world of things identified as or related to "object-oriented programming".)
But I wasn't around at the time...
* How do we make programs that are easy for the machine to execute efficiently? * How do we make programs that are easy for humans to read and understand? * How do we ensure, given the maintenance requirements of our programs, that another human who doesn't have the benefit of our experience can safely make changes to our programs without unitended consequences?
Discussions around OO versus Functional versus Procedural miss the point. You can write perfectly maintainable procedural, functional, or object-oriented code. If you're authoring something brand new you have to approach it with a complete understanding of all the moving parts. If you're not there, make a prototype, wait a few days, then go through and re-read it. Anything you don't understand is something nobody else will the first time they approach your code base. Come up with ways to be explicit and to communicate clearly what the intent is. Try to anticipate what things people will be changing often and make those easy things to change. Remember that it's about conveying a representation, not a deep understanding. You want to represent your understanding of the problem space to someone who doesn't have the same level of understanding as you.
Well, you say we're having wars around the programming paradigms, I say we're having "spirited debate" around the principles :). I've been working mostly in Java for the past 20 years or so, and I can't help but observe that most people, when they try to put together a Java application, default to a sort of design that looks an awful lot like old Cobol programs did: they have a "datatype" generator (usually automated from XML schemas) and a slew of "utility" classes with mostly static functions that have mostly static data that operate on these datatypes, and as little class instantiation as they can possibly get away with. I've seen this same basic architecture repeated many times across four different employers in two decades. It's always a lurching, monolithic, untestable behemoth that never works reliably and resists any attempt to change. In talking with the original designers, it's clear that there were no principles behind the design besides "it still doesn't work, how do I get this thing to work". If there were clear and adhered to principles like automated testability, you'd end up naturally with an OO (or even better, FP) type design.
It seems to be improving in the last 5-10 years, as most practitioners have found that both of these eyesores can be reduced. DI (sometimes messy itself, but it can be done cleanly) tends to make people rethink those utility classes, and shallow inheritance is now favored, with a focus more on interfaces and composition.
IMO classes as false separation is the reason we kept C++ and Java out of Oses. ABI compatibility, dynamic loading, etc are all from the duck philosophy. It is no one's business if your duck thinks it is a duck.
https://www.edn.com/5-simple-steps-to-create-an-abstract-dat...
And then you have nicely "namespaced" functions that operate on those abstract pointers and your code is isolated from the implementation and you don't accidentally depend on some implementation detail you shouldn't.
But ultimately this is all just informal OOP. Objects/classes are just a natural way to organize programs.
The only downside is that it's not as popular in main stream and there aren't like ECS first languages.
However I believe that there is a reason ECS has been dominating in the game industry for the last 20 years. It's very flexible, fast and works nicely with the GPUs.
That there are good reasons in terms of memory layout to locate all of those values which would have been member variables in large arrays so that external functions can iterate over them efficiently, but that in so doing, private members become impossible.
What am I missing?
In your example, would some of the values be private while others public? Like in ECS, they are either all private or all public which kinda makes sense.
From my perspective, ECS looks like an optimization where you make the engineering tradeoff to sacrifice data hiding. For very good reasons! Memory locality is super important when iterating over elements the way it is typically done in ECS, such as when rendering objects in a game.
If you consider your app a machine that transforms A to B, you don't want data-hiding in the first place. You want to be honest with the data you have and data you need.
Also in most web apps: Your data will escape your precious objects (mostly as JSON).
And don't confuse data-hiding with having objects in a consistent state (i.e. mutating data). Two different things.
The usual argument against OOP that I hear is that implementation inheritance is brittle. That, I understand and agree with.
But data hiding being bad? I'm not persuaded. The whole divide-and-conquer aspect of breaking modules into smaller parts requires not exposing unnecessary details as interfaces.
You have a starter motor in a car. If it's only connected to the motor via a couple wires, then it's easy to design a better one and install it. But if it has dozens of wires connecting to every which part of the engine, improving it is much harder.
> Also in most web apps: Your data will escape your precious objects (mostly as JSON).
My "precious objects"? :( Can we please discuss this subject without getting into a flamewar?
ECS takes a fundamentally different approach. OOP is about breaking things down, ECS is about building things up.
Like in ECS, it's possible to dynamically toggle whether an entity has a particular field or not.
Well, will your data escape or not? As long as you are staying in OOP-land, everything is as it should be: Objects passing messages to each other, as politely as possible (trying not to bring in the concurrency aspect)
But then, there comes a REST endpoint around and wants your data. Now it is out in the open and subject to inspection and change. So what have you gained by using data-hiding? Shouldn't you be able to pass that data around openly in the rest of your code as well?
Maybe I shouldn't say data. I should say values. Immutable ones. Values can be passed around safely. They can be inspected by anyone. Accessed by multiple threads etc. That allows for abstractions that are impossible to achieve with stateful objects.
In OOP we keep our state hidden in the objects, have methods manipulate it, but that means these object can't be a value. They can change anytime. You can't even observe those state transitions (unless you code for them, which gave us bean-style properties...)
To stay with your motor example: If you assemble a motor from values and start it, it will continue to work, even if someone installs a new part. You can't break the motor.
You can even save the motor to disk at any time, load it back and you still have a working motor.
Yes, all of this could be achieved by using objects talking to each other, but it is vastly more difficult (try to snapshot a consistent state of an object graph, for example)
The answer to that meta-question is "to preserve assumed relationships between data." `std::vector` stores a capacity, and that capacity damn well better correspond to the length of the last array it allocated or you will get a segmentation fault. So you hide it.
The OO thing that ECS is pushing back against is a tendency to group things together solely because they are in one-to-one correspondence, when they are otherwise unrelated. In a game, there is no constraint that needs to satisfied between a player's HP and their position on the map. So why "hide" this data together in an aggregation?
If you want to permit only a few functions to operate on your big array 'o stuff, you can always do something like
class sensitive {
static allowed_operation(sensitive& s, context& c);
private:
double here[N];
int be;
bool dragons;
};
Similarly, if you need to do some really complicated computation on everything about a player, you can define Objects as aggregations of indices into the arrays 'o stuff (where typically people think of an Object as a big-ass struct of structs, often implicitly due to Inheritance), where the data you care about hiding is not the stuff from the array, but the indices: class Player {
GetComponent() { return arrComponent[index]; }
private:
size_t index;
};> But you also aren't asking yourself the meta-question "what is the point of data hiding?"
My answer to that question has always been that data hiding is necessary for the sake of modular independence in large systems. This is a principle which applies across all of engineering, not just software — see the "starter motor" example elsethread.
Implementation details need to stay hidden so that you need only concern yourself with local effects when making changes — instead of needing to keep the entire system in your head because any change might impact any tiny detail anywhere at all.
Nevertheless, I agree that in some circumstances it makes sense to expose the data structure as an API, and that ECS offers a compelling approach and set of conventions as to how you would go about that.
In ECS the things you talk about are achieved differently.
In ECS, implementation details are local to the systems that process the relevant data. You don't store implementation details in the data you pass to the higher abstraction.
Yes. But not on a granularity of every object. Just like you don't put a padlock on every pocket, to protect your left hand from grabbing stuff from a right pocket.
Hiding data has a real cost, just like inventing abstractions and interfaces for every tiny thing. That's the core reason why OOP-software is so bloated and always feels so "heavy".
The right granularity is much coarse: modules, API layers, data-stores. Much closer to service in "micro-service", than "object".
> It (OOP) made it possible to develop much larger programs than before, maybe 10x larger.
Why?
This contradicts my intuition, because the biggest problem is that OOP focuses on state changes (of objects). State change means complexity, so formally, you need to understand the set of all possible states to reason that the program is correct. This is an exponential problem in the number of pieces of state, unfortunately. So how would this enable larger programs? What's the reasoning here? It would mean some even more potent mechanism of OOP makes more state more manageable. The structuring of data does not strike me as that potent -- nice, yes, but, well -- please explain!
> OOP provides a way for programmers to organize their code.
I think it is primarily about organizing data. The organising of the corresponding code follows. When Java emerged, it did not even have functions -- every piece of code had to be attached to data. In SmallTalk, you could change the definition of the global 'true' or even extend bool to have three values -- definitely data centric, and introducing unmanageable state...
> 100% pure functional programing doesn’t work. Even 98% pure functional programming doesn’t work.
I believe this. Well, intuitively, I'd say no paradigm works well when applied 100% pure. It would still be nice to see examinations of this or reasoning or proof or at least examples.
This is just not true. In fact that's the whole point - you encapsulate state. It becomes easier to reason about.
When I write a webserver, do I need to know or care about the internal state of the classes dealing with the TLS protocol? Or do I encapsulate consistency and correct behavioir at that level and then use the interface elsewhere?
I don't need to know everything at all layers.
I've never understood why so many people insist on this false dichotomy.
I do both.
These past years, I feel that I've been writing much better functions as my understanding of more advanced concepts has ramped up, thanks to functional programming and the emergence of things like Rx and coroutines, but once I have these functions, I am extremely happy to be able to organize them in classes and modules and leverage various forms of polymorphism to make my code base flexible and easy to maintain.
> I do both.
Are you talking about methods attached to your classes or functions that are at the root/module level? If it's on a class I would argue it's not a function since it takes input that's not just the parameters to the function. If it doesn't access properties of the class, then whey does it need to be attached to the class?
For example, let's say there is a function that takes instances of two different classes and returns an instance of another class. If you want to move that function to a class, where does it go?
> once I have these functions, I am extremely happy to be able to organize them in classes and modules
I understand the organizing into modules and being able to create interfaces/types, but why classes?
If that statement is true then OOP was surely the most important advance in software development since the original higher-level languages, such as Fortran, were developed.
My experience agrees with the author's stance that OOP provides significant leverage in developing complex software. What I don't understand is why he then falls into the current fad of seeking to abandon it.
Surely if OOP has been as successful as the author claims then what's needed is not to replace it with another paradigm that rejects all the core principles of OOP but rather an approach that combines the good aspects of OOP with other paradigms allowing the programmer to use the tools best adapted to the problem at hand.
"the problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle."
In a nice procedural or functional program, when I need to do something I just write a function that takes the relevant arguments. From the function signature, I can reason about the function, that it only depends on and interacts with those things I passed in. It also means I can reuse that function anywhere else in the code I happen to have some of those things that are the function args.
In the OO approach, even if a function only needs to access a few member variables, I still pass in the whole object every time I declare a member function. This makes the function harder to reason about, as the body of the function could touch any member of the class, and if it has other classes as members then it could rely on any of them too. It also makes the function much harder to use, as even if it only needs X, Y and Z, calling it requires creating a whole instance of that class of which the function is a member, which may require a bunch of other stuff.
In that sense, the OO approach introduces unnecessary coupling, between the logic in the body of a member function and the class members not used in that function. Because I can't use that function's logic without creating instances of the members not used in that function.
Now that people are mostly over inheritance for the sake of inheritance, it's not so bad. Often, you want to have one class be a member of another, rather than a child of it.
This brings up a common problem. If A is a member of B, B sometimes wants a back-reference to A. This is hard to set up in some languages. It should be made easy, because it removes much of the need for inheritance. For Rust, especially, it's a special case of Rust's back-pointer problem, the one that makes doubly linked lists and trees with backpointers hard. Not having this encourages the creation of structures with too many data elements.
That said, I do agree with the sentiment that multi-paradigm languages offer higher developer productivity than purist languages. I’d still have procedural style in fourth place, behind OO, FP, and Logic.
Most importantly, I think this is just all talk. This is John D. Cook just spitballing with a contemporary and debating things. I bet the other person in the conversation is probably not all that convicted of his argument and is throwing an exasperated take out there...
I’m talking about CSS, where class inheritance is called the “cascade.”
To create a program you used cables on a plugboard. You had a general purpose computer that can run multiple programs, but in other to switch programs you had to spend significant time setting up cables. A small change in the program may also result in the same thing.
So then we moved to stored program computers, so that we don't have to do that anymore, and the global consensus is that it was a good idea, because it saves time.
Then, we had programs in bare machine language. But that poses various challenges:
1) Having to keep track the state of memory and registers is difficult. So programming languages were created to create an abstraction over them in the form of variables.
2) Programs were full of jumps that became hard to track and maintain at scale. Structured programming was created to create an abstraction over jumps in the form of control structures (sequence/selection/iteration/recursion). Procedural programming was created to group statements into reusable procedures and functions.
3) Then, having variables around became hard to maintain as well. So then structures were created as a way to group variables that are used together.
But then, people understood that some procedures and functions are coupled with structures, and that some structures are supersets of other structures. And that's how OOP was born.
In this mindset, I do not think OOP is a bad idea. I also do not think that the natural consequence of OOP is bad software. The problem is how the paradigm is used, not the paradigm itself.
The problem people are facing now is shared mutable state, which is not only a maintainability problem, but is also problematic in multithreaded software. Functional programming is a viable solution to address that problem.
NOTE: edited based on suggestion, since apparently I got some concepts mixed up.
function unstructured_summation (int lower, int upper):
int sum := 0;
label loop:
sum := sum + lower;
lower := lower + 1;
branch_neq lower, upper, loop;
return sum; // this is assuming there's even a return concept
This is short and simple, so that goto or branch is fine here for reasoning about. But it doesn't scale very well [0]. Structured programming discouraged these ("discourage" being a scale from: don't use unless it makes sense to don't use at all) in favor of other constructs that were more clear in their intent and easier to reason about (particularly in the days when you couldn't write a piece of code and test in in 10 seconds or less).[0] I once inherited a Fortran codebase that was essentially written in an unstructured style, many gotos and computed gotos. The guy who wrote it (yes, singular) over a 20 year period knew exactly what it did. But that didn't help me, because he was 70 and retired with a nice pension. The whole thing had to be rewritten using the original as a reference for testing because it was unmaintainable.
What does "85% functional" mean? How exactly do you measure that?
For that matter, what does "OOP" mean? PG published a note from Jonathan Rees [1] which lists 9 possible (pieces of) definitions.
"Because OO is a moving target, OO zealots will choose some subset of this menu by whim and then use it to try to convince you that you are a loser."
That's roughly what I'm seeing here, yes.
Why can't people let paradigms be paradigms instead of trying to turn them into religions? They're approaches that might or might not be well suited to different problems; yet so many people try to make the problem fit the paradigm they prefer instead of the other way around.
This article is thankfully more nuanced than most on this subject, though.
<include stdio.h> ... FILE myfile;
Isn't myfile really an object?
My jaw dropped. Maybe they were being a smartass, but that's just crazy talk.
Like all dogmatic stances, people just take a "My Way or the Wrong Way" approach.
Most of my engineering is a hybrid of classic (read: "old") techniques, and new, "cutting-edge" techniques.
I write about my outlook on that here: https://medium.com/chrismarshallny/concrete-galoshes-a5798a5...
(Scroll down to "It's Not An 'Either/Or' Choice").
One day, AIs might replace us poor coding schlubs. At that point, we can assume that everything will revert to Machine Code.
Until then, OO is a great way for humans to grok the complexity of software development.
I will always keep refering to this classic joke, when I encounter inflexible, dogmatic thinking: http://www.solipsys.co.uk/new/TheParableOfTheToaster.html
However, these were people answering Swift questions.
Swift introduces "Protocol-Oriented Programming" as dogma, which has some really salient points, but is not the Philosopher's Stone of software development.
EDIT: Added "as dogma"
Hmm...that would be message-oriented programming, which is what Smalltalk for example has been all about. First equating OO with heavy use of static typing and inheritance (which wasn't the idea), noting that that doesn't actually work (surprise!) and then "inventing" POP...sigh.
https://blog.metaobject.com/2015/06/protocol-oriented-progra...
In Smalltalk, the messages an object responds to are called its protocol.[5]
"Every object in Smalltalk, even a lowly integer, has a set of messages, a protocol, that defines the explicit communication to which that object can respond."
Now that that single protocol for the entire object can be split into multiple meaningful protocols should be obvious, but Alan Kay has also explicitly talked about objects embodying "multiple algebras"[6]
The basic idea of "protocol oriented programming" is that you focus on what an object can do (its interface/protocol) rather than what it is (its class/type) and that Smalltalk certainly did. Whether you check this statically is a secondary and certainly orthogonal concern, but of course there were and are statically typed variants of Smalltalk (such as Strongtal) and type-systems for others Smalltalks.
Traits were introduced in 2003 for Squeak Smalltalk.[7]
[1] https://en.wikipedia.org/wiki/Objective-C#Protocols
[2] https://developer.apple.com/library/archive/documentation/Co...
[3] https://cs.gmu.edu/~sean/stuff/java-objc.html
[4] https://blog.metaobject.com/2014/05/the-spidy-subset-or-avoi...
[5] https://www.cs.virginia.edu/~evans/cs655/readings/smalltalk....
[6] http://www.cs.uni.edu/~wallingf/blog/archives/monthly/2014-0...
As actually intended (listen to Dave Abrahams clarify what he meant on John Sundell's recent podcast) it has much more to do with parametric types and functions that operate on them, and is almost directly inherited from ideas that have been in C++ since the STL.
Misinterpreted?
Sorry, but the definition of a protocol, particular in the context of Swift/Objective-C has been fixed for around a quarter of a century. And of course, those protocols were what he talked about in his presentation.
If he meant something different, possibly he should have (a) picked a different term and (b) talked about something else at WWDC.
I get a bit discouraged, when smart, energetic engineers (usually young), latch onto the buzzwürd du jour, and declare it to be "The One, True, Path; all heathens and apostates must be cleansed with fire."
I say that, as having done exactly that, myself.
Looking back at my thinking, then, makes me wince.
Perhaps this is just a symptom of "measure some aspect of a person's output, and that person will optimize for that measurement, no matter the cost to the rest of their output."
> Until then, OO is a great way for humans to grok the complexity of software development.
Particularly in domains where everything is an object, like game development.
I thought it was an aggregation (as opposed to hierarchical) model. I've been doing that with OO for decades. It makes a great deal of sense, in some cases. PoP has similarities to that, but, in my experience, there is absolutely nothing that can compete with polymorphism and/or inheritance -and, here's the kicker- in some situations.
OO, in my experience, is a lot different from just polymorphism. I think that it's a way of conceptualizing the model. I used to write OO in straight C, in the late 1980s. I called it my "Faux Object" pattern. It even had a rudimentary form of polymorphism (no vTables - just function pointers that could be replaced -Poor man's vTable).
My experience is that lots of people have difficulty adapting to hybrid design. We learn one tool, and we learn it well. We go to conferences, where we are told that we are über-programmers, because we use this tool.
But try using a screwdriver to drive a nail. Most of my friends that are (building and construction) contractors are quite used to hybrid design. They use one technique and set of tools on the foundation, another on the walls, and yet another on the roof.
Some of these aspects are a large enough field that they can specialize (like framers or roofers), but there are quite a few "general" contractors that are capable of building a house from scratch.
I started off with Machine Language (the original "ML"), and have written device interface code for a long time, and am quite used to a lot of limitations (like "lite" versions of languages for lower-layer work or embedded content). We use the best tools for the task at hand, and don't sneer at others that don't use them.
(Maybe this is also getting mixed up with things like data oriented design?) I am definitely not an expert in these things so I would welcome any feedback.
They might be using discriminated unions, or type-distinguishing enums on rows or structs with a superset of attributes, but the concept is hard to avoid when trying to generalize behaviour over heterogeneous data.
modularity is VERY good
(I think most of the shortcomings of C are a lack of modularity)
functional programming is good (eliminate side effects and people and compilers can assume without making an a * * of u and me. Should not be required)
classes are good
(frequently it's the same, but slightly different)
multiple inheritance is bad
(there are other ways to do it)
This speaks to me so much.
FWIW I like Bob Martin's definition (encapsulation, inheritance, polymorphism).
----
One thing I hate though, in a language like Java, is when I see "utility" classes with static methods which take objects as parameters, then perform some calculation based entirely on the state of the passed in object. In my opinion, if the object can reason about its own state and return an answer based on that reasoning, that method/logic should be in the object, not elsewhere.
Another one that bothers me are these transformation static methods which take type A and return type B based on nothing but the state of type A. The languages were talking about, C#/Java already provide a facility for this called a constructor.
If the development approach is going to completely remove operational methods from data types then I'd think long and hard about using a language which supports this instead of a language, like Clojure, which does not.
----
> that method/logic should be in the object, not elsewhere
Only if it's involved in preserving some object invariants, or accessing parts of its state that are abstracted away in the public interface. Otherwise, you're breaking encapsulation by putting some logic in the object that doesn't belong there, and making it hard to change the implementation later.
> The languages were talking about, C#/Java already provide a facility for this called a constructor.
There are sensible reasons to avoid using constructors for this, at least in the general case.
----
> Only if it's involved in preserving some object invariants, or accessing parts of its state that are abstracted away in the public interface. Otherwise, you're breaking encapsulation by putting some logic in the object that doesn't belong there, and making it hard to change the implementation later.
I've been pondering this thread all day, and I think this is the crux of the issue. Ideally, classes would only encapsulate state and you'd use namespaces/modules to encapsulate functionality. But most Java/C# OOP examples use classes to encapsulate both state and functionality, which gets you stuck in the morass that the article discusses.
----
OOP in sense of message passing is just a programming language implementatiin technique used in Smalltalk which lets you handle functions of multiple arguments with ease. You basically 'pass' each argument as a message to the receiving object.
That is obviously just a hack to make that feature of nontrivial function call work. Real message passing obviously has to be asynchronous to even resemble concept of real life message passing. Erlang and Actor model in that case are pure models of message passing based OOP.
Somehow all of these OOP systems are just distractions from solving problems using computers because you can represent your problem using pure 'data' such as numbers or strings and composing them to bigger structures with well defined operations - such as lists, dicts and tables and so on. Objects can be purely abstract thing in your mind or documented in code comments.
Imperative programming obviously doesn't scale beyond one machine because of von Neumann bottleneck and suffers from unmanageable global state. That doesn't mean OOP is the solution.
Functional programming is of two kinds - statically typed with powerful type systems - which are systems with very limited field of use and high development cost. Unfortunately unusable for real life problems because of lack of intuition.
Other less strict functional languages fare better, but used naively suffer from von Neumann bottleneck too. Unless they are used in a language oriented programming paradigm, whereby you use your language only to construct a higher level language DSL) which can abstract over more than just a single machine.
Good examples are Lisps and their ability to define other languages such as Prolog or SQL or Linq.
The only way to make parallel computing possible is to use data parallelism instead of task parallelism so data oriented languages such as sql or datalog are the future since Moore's law reached its end. This poses a strong constraint on design od programming languages.
SQL, while not suffering from single processor bottleneck, unfortunately sucks as a language because it is not programmable and nondeterministic in generating physical plans.
Which offers a great opportunity for programming language enthusiasts - build a programmable sql or datalog like language because other paradigms reached some fundamental constraints and hit a dead end.
Or come up with a new paradigm.
Most OO code is poorly designed, slow, error prone and a memory hog. Every object you initialise wastes so much memory. OOP is not going to be there forever. It is a failed paradigm. Hopefully it will be replaced with something more efficient, faster to develop and something with more provable correctness.
Is an object a data structure, an actor, a module, a knowledge frame ?
A state machine can rightfully be called state + behavior. Sadly no programming language has made hierarchical state machines a first class feature to support with syntax despite it being software engineering unlike what OO bros claim.
The only success for OO has been UI development. It's a failure everywhere else. OO databases are a failure. ORM is a failure. OO based distributed computing has failed, we use REST. OO based design is a failure, no one uses UML.
OO based architects are a waste of time and money. You can replace all OO based architecture nonsense and patterns with code written in Go.
As such I don't think the rest of your post is worth consideration.
>Design patterns are not engineering, let alone architecture
>Sadly no programming language has made hierarchical state machines a first class feature to support with syntax despite it being software engineering unlike what OO bros claim
What do you mean by engineering here?taking in a pointer to a struct as an argument is much less overhead for the programmer than "ok this is an 'object' here is why it is doing what it's doing oh here is this method etc etc"
EDIT: typo
But in the end we should be less dogmatic in general . Myself and a lot of people I know started out building big inheritance trees only to find out that this didn’t work so well so we reduced it to a degree that was useful and manageable. But then you have some very loud dogmatists who scream “this is not OOP” and insist on building factories and factories of factories and all the other nonsense. And for some reason the dogmatists often get heard, maybe because they are so loud and convinced of themselves.
If you focus on the compositional relationship you get a lot more mileage out of OOP which is the real revolution over the past 15 years. And the decomposition is how you arrive at the Single Responsibility Principle and Encapsulation. Suddenly you can work only with the set of all Complications, or the Face, or Time or Date without worrying about which Watch you have.
If you build a library or framework, you'll use it more. There are often plenty of literal "is-a" relationships that don't exist as often in application code. So you probably benefit from inheritance in OOP quite a bit even if you aren't building those relationships yourself.
Alternatively, spending a lot of lead time doing OO analysis in such cases is more like premature optimization. Instead, if the problem does not appear obviously OO, just implement it as procedural/structural, but implement it properly trying to keep the scopes and states as limited as needed. If a better model, perhaps OO, could emerge from such implementation, then it could be applied in the next iteration.
Simpler object-based paradigms and languages, which feature "classes" and "objects" for encapsulation and data abstraction, plus maybe some interface-only subtyping, are used all the time and work quite well with functional programming concepts.
In every case, focusing on dogma avoidance and having reasonable technical debt levels seems to be far more important than the specific language and paradigm.
Moreover, advanced compilers (like GHC and the OCaml compiler), can often turn functional code that allocates and copies into in-place, GC-free code.
My point was that this seems like the same mindset even in two fairly different domains, and that we’re prone to talking about it as a technical problem when it’s really more of social one.