60 comments

[ 1.7 ms ] story [ 127 ms ] thread
> My own feeling is that object-oriented programming is a useful technique in some cases, but it isn't something that has to pervade every program you write.

That could apply to pretty much any programming paradigm.

I wouldn't use object oriented programming for small programs. Why bother with types when you can express things in a couple hundred lines of python with functions? Why write classes to munge some data that could be better expressed as a series of map/fold/etc. on lists of tuples?

However, for keeping large codebases approachable and maintainable, OOP is the best paradigm I have personally seen.

It depends what you mean by OOP. If you're talking about class orientated program (aka everything is a class) then I disagree. That can get painful to maintain in large applications, especially when concurrency becomes important.
> I wouldn't use object oriented programming for small programs. Why bother with types when you can express things in a couple hundred lines of python with functions?

Hm? Types don't require OO. Even your 100 line program can benefit from type checking.

Approachable code must be structured in a meaningful way. That doesn't require OO either.

>> Maybe I'm just stupid, or have worked on some limited subset of applications.

It seems to me that Graham answers this question in the preceeding paragraph:

>> I've done a lot of things (e.g. making hash tables full of closures) that would have required object-oriented techniques to do in wimpier languages, but I have never had to use CLOS.

He simply writes ad-hoc implementation of an object system, instead of using one provided with his favourite language. Seems like a bad case of NIH syndrome.

> He simply writes ad-hoc implementation of an object system, instead of using one provided with his favourite language. Seems like a bad case of NIH syndrome.

Could be. Or maybe he just accepts OOP as a necessary tool in a small percentage of cases and opts for the lesser of the two evils -- rolling his own mini-OOP system as opposed to accepting all the baggage that comes with a first-class OOP language (like side effects and bloated code). Remember the saying "when you code with an OOP language, you not only get the banana, but the gorilla and the whole jungle".

I know I would defend OOP to my dying breath just a short 2-3 years ago. But when I started working with FP languages I discovered that OOP is vastly overrated.

I am not as naive as to think that I will change your mind. Just offering another perspective.

I don't think making a hash table of closures is necessarily equivalent to "object orientation". Sure, if you use a set of well-known keys to represent an interface, then it's headed that way. But the same structure can be used for other things, e.g. a table of opaque IDs to closures encapsulating a possible "next step" through a workflow in an interactive program. Indeed, I think the original news.arc did exactly that in many places (and contemporary Hacker News still does somewhat?).

(My reading of the "wimpier languages" bit was that it was probably a dig at Java, where [prior to Java 8], you had to explicitly write an inner class to emulate a closure...)

I've always disagreed with pg's contention that objects are the poor man's closures, at least w.r.t. Common Lisp. I write a lot of Common Lisp code and almost every one of my programs uses CLOS in some way (and many of my programs use closures too; they're complementary). I'd probably never use CLOS if it limited me to single inheritance or single dispatch or prohibited reclassification like most OO systems do, but it doesn't. CLOS is OO without the arbitrary restrictions.
I'd say this more or less the mainstream view these days. Both Go (2012) and Rust (2015) decided to do polymorphism with a trait system rather than inheritance, for example.
Neither Go nor Rust are mainstream -- that's still C++, Java and Javascript.
New languages can reflect mainstream opinions even if the languages are not mainstream. Mainstream languages tend to represent fossilized opinions from the time when they were designed.
Go is #12 on the TIOBE index, and by that measure more mainstream than Perl or Ruby. If by mainstream you mean "the top 3-5 programming languages", then you're not talking about anything invented in the last 20 years, which is kinda the point.
If we were to use a car analogy: go has been in production for the last few years and there's a ton on the road, but the average car on the road wasn't built in the last few years.
The mainstream is not only defined by what's most popular, but also by what sets the trends. The masses always get the message with some delay...

Besides, all of the three languages that you've mentioned are turning more functional and less OO with each year...

JS still has OO (and recently got "classes" as sugar), but it's not used in the crazy deep-OO-hierarchy style at all.

Java, that used to cherish that style also abandons it nowadays (with closures, streams, etc.).

I can count on 0 hands how many times I have used inheritance in the past 6-7 years. And if you don't count times that a framework requires me to use it to accomplish something, like React.Component or NSTableView, then that number goes back something like 15 years.
It boggles my mind that inheritance is still taught in schools and universities. It's so easy to debunk the common lovely idea of "Square and Circle are a type of Shape, Rectangle is a type of Square" that it's not even worth talking about it -- you might want the square to be rendered in a GUI framework but have the Circle rendered only in your PDF reports, being one very lame example I can think of in under a minute. Many others exist on the net and they are much better than mine.

(Traits of types -- and thus multiple inheritance -- make much more sense if you so want to model the real world.)

"OOP is necessary" is a lie that is worth billions of dollars. Inheritance ends up being leaky and non-isolated. Every time.

Inheritance works great in mathematics because then it's all about using some laws to compute derived properties, but in programming we use objects for behavior with side effects. So it's like apples and oranges.
I’m not really sure where you think mathematics is full of inheritance. Do you have an example?

Off the top of my head:

Subsets, sub-anything-else, not really inheritance

Definitions of the form an X is a Y with the following extra properties, I think don’t really count as inheritance either. Example: A ring is a group with ..., but if you wanna to prove anything interesting about a ring, you can’t kust use the group properties or you would have proven a theorem about a group, and so the only useful thing about the group structure is that you know what it is. Any useful theorems will need other useful definitions (eg homomorphism) and these don’t get inherited and so the isomorphism theorem for groups doesn’t become the isomorphism theorem for rings “for free”. There’s also the fact that a field is a ring plus an extra property but most ring things become basically useless because much ring weirdness becomes trivial in a field and what is not trivial in a field is typically impossible in a ring, so knowing about rings doesn’t tell you much about fields.

Example: a topological space is a set plus a subset of its power set such that ... but all the interesting things in topology typically have little to do with those sets and more to do with eg functions (but not just any set function) and the added properties.

I suppose the theme is that one adds structure to (ie inherits from?) something because the new structure bans a lot of the weirdness of the bigger thing and allows for more interesting things to be done. The structure of the bigger thing is therefore not so useful because what is known about it is less relevant once more structure is imposed.

In applied maths, the Fourier and Laplace transforms are part of a more general family of integral transforms yet this knowledge is basically useless in their application and it would not make much difference if they were not related.

Fourier series fall into part of a bigger theory called Sturm-Liouville theory but in many of the places one uses a Fourier series this bigger picture is not relevant whereas the specific details about Fourier series are. This is not to say that Sturm-Liouville theory is not useful.

If you're merely a consumer of libraries and frameworks then you won't use inheritance very much but if you're a designer of libraries and framework will use it almost all the time.

I love that inheritance lets me customize 3rd party code and integrate with it smoothly; nothing pisses me off more than a "sealed" class that you can't inherit from.

That's exactly what I hate about Cocoa. Sure Carbon wasn't perfect, but subclassing NSTableView so that you can inject your subclassed NSTableHeaderView just to make some UI tweaks is awful. The entire framework is like this, and Swift doesn't help much. I get that it's a legacy design due to performance issues back from when Cocoa was created 25 years ago, but I would absolutely love to see a modernized macOS UI framework where functions were always used in place of delegates or subclassing, and where lexical scoping is the backbone of your app's own internal architecture instead of class hierarchies.
> functions were always used in place of delegates or subclassing

I'm not sure what this means? In your modernized UI framework, how would you want to be able to make UI tweaks to built in OS/framework components?

I've seen a few other system for customization and they all tended to be worse than just being able to subclass internal classes and override methods. Now I admit this can be terribly brittle but often the alternative is no way to customize at all.

I guess instead of overriding methods like “windowDidLoad”, you would set a function (I mean block) as “window.loadHandler” or something like that.

I don’t really see why this would be better, to me the two approaches seem isomorphic...

Use higher order functions to customise behaviour. I.e. your hypothetical NSTableView would look like this:

    nsTableView ::
      { renderCell :: a -> NSView   -- this is the function that renders a cell given an `a`
      , header     :: NSView
      , footer     :: NSView
      }
      -> [a]
      -> NSTableView a
I.e. pass in a function that renders a cell for a given `a`, a header and a footer NSView and whatever else, a list of `a`s and get back a NSTableView which displays that list of `a`s.

React calls this pattern higher order components iirc.

As the other commenter pointed out, this seems pretty isomorphic to virtual methods.
Two obvious differences off the top of my head:

1. Overriden methods have access to the internal state of the parent class, which is almost universally a bad idea.

2. Since functions are values, you can freely mix and match different view configurations without having to create a subclass for every permutation.

#1 isn't true; you only have access to the state that subclasses are allowed to have (assuming you're using a language with any protection at all).

#2 is a good point. But there are some limitations as well. You don't get to augment the default implementation unless it is also exposed somewhere (passed into the function).

Both methods have a lot in common with the obvious differences that you mention. A class with no public methods and a empty virtual function is equivalent to a class calling a higher order function in the same place. If that virtual function has a implementation, you can get the same functionality with a higher order function by passing in the base implementation as a parameter. For full equivalence, you could also pass the object instance as well. Add in protected methods and/or state and you can get that with a second class that implements an internal API and again pass that to the higher-order function.

Ultimately you can achieve all kinds of equivalence this way. I think there is some advantage to using the terminology in the language (private, protected, virtual, abstract, this/base, etc) to describe the more complex situations. Having a simple function call is useful for more simple situations. This is the classic struggle.

> integrate with it smoothly

"Smoothly". For my 8 years with Java the only base classes that haven't been retroactively modified and thus breaking my inherited classes... were the ones in the stdlib. Everything else could and has been pulled under my feet, many times.

Everyone and their moms and dogs was like "hey, I think I can do better because I feel great this week!" and then 10 of us had to repair an object hierarchy of 200+ classes. I mean, everybody knows the CTO can't make a mistake, right?

And don't give me the "not in a disciplined team!" argument. Even disciplined teams make mistakes because often the author has no idea how many other colleagues use their base class.

I know the idea sounds too good to ever believe that it can't be true -- but yeah, it's not true.

> nothing pisses me off more than a "sealed" class that you can't inherit from

The "sealed" classes are meant to be semantic boundaries. Get an instance of it in your object and be done with it. What's so triggering about that?

But you don't have an alternative. You subclassed things and someone broke the base class. It happens. But there is no other magic bullet (whether it be all functional programming or something else) that solves that problem. Most just make extension impossible, which isn't really better.

> The "sealed" classes are meant to be semantic boundaries.

The author of the "sealed" class definitely imagined all possible use cases and their code has absolutely no bugs, right?

> The author of the "sealed" class definitely imagined all possible use cases and their code has absolutely no bugs, right?

...What?

You can compose an instance of the sealed class in yours. You lose all brittleness from inheritance in the process. I've been coding OOP most of my life and I am working with FP for 2 years now -- composition never introduced a bug. Sure it's anecdotal but after a trend repeats itself 5+ times, you start observing patterns and making conclusions.

Additionally, a number of FP languages offer Java-like and Go-like interfaces for an extra peace of mind.

> You can compose an instance of the sealed class in yours.

But I can't subclass a sealed class to fix a bug or add a feature and pass it back into the framework to use. And yes, I have needed to do this otherwise I would never notice it was sealed in the first place.

Can you elaborate on what you do when inheritance feels quite natural? A classic example would be to allow a plugin. For that natural thing to do would be to define base abstract class with bunch of overridables, utility methods and pure virtual interface methods. I can imagine it doing without OOP but it doesn’t seem as clean or natural.
One of the challenges I’ve found with OOP is that’s really easy (encouraged!) to use a lot of “anti-functional” patterns.

A functional style is not the only way to write programs, but it’s often much easier to reason about behavior when you’re not chasing side effects.

And OO implementations often have a LOT of side effects.

>And OO implementations often have a LOT of side effects.

If you're worried about side effects and want pure functional programming... Arc is not the language for you.

I think pure functional programming is often worse than a looser style which one often wants. This is sometimes described as functional core, imperative shell.

The idea is to try to keep state in values (rather than global variables) and to update those values in a functional,typically immutable, maybe a sort of careful mutable way. Then on the outside there is some global state of things that are treated in an imperative way, eg setting up loggin, or a global variable for “the state of the world”, or reading user inputs, processing, and outputting.

IO is something that Haskell has to make a big deal about because it is lazy and “pure.” One can think of it a bit like async in JavaScript: (most) IO things are async and so an async function is one that does IO, just like any function returning IO a in Haskell. I think in most programs structured with a functional core, having IO in the core will typically mean having logging or debug printing which I think is not really bad and not worthy of eg polluting type signatures everywhere. Most meaningful IO should go in the outside, topmost bits of the program because that’s where it belongs (rather than because your language makes it so painful that you don’t want it any other way).

I suppose my point is that I don’t think “pure functional programming” is something one should want throughout a language.

> I suppose my point is that I don’t think “pure functional programming” is something one should want throughout a language.

Yeah, same here. Haskell struck me as too strict and clunky to get basic stuff done. Its focus doesn't seem to be productivity. Hence I gravitated towards Elixir (and lately trying Clojure). It allows me to get stuff done in a small number of lines, is amazingly well documented and has powerful primitives.

The pursuit of academic purity quickly turned me off of several languages, Haskell included.

I personally prefer fully object-oriented languages, as long as it's an option and not a must like in Java.

Replacements for OOP are just another fad, in twenty years from now people will come up with new fancy object-oriented languages, maybe functional ones with immutable objects, or with some other paradigm that replaces traits, mixins, interfaces, etc.

OOP is good for domains that naturally give rise to a clearcut inheritance structure, such as stateful GUI frameworks or ontologies that directly mirror real-world objects. It's not a magic pink unicorn, but that's true about all other approaches.

If you want everything but small, compact executables, you can already have that today in CommonLisp. It can be used to write functional programs or object-oriented styles and has almost all other programming language concepts you can imagine. Still not many people use it. The choice of the latest programming languages has almost nothing to do with their features and more to do with the availability of tools, libraries, and programmers.

> availability of tools, libraries, and programmers.

These can -- and have been -- influenced by corporations. Many times.

It's a vicious cycle.

I don’t think so. Go is picking up steam with no sign of slowing down, and the more experience I get, the less I write OO code (I basically only use inheritance as a poor-man’s substitute for automatically delegating to a member, a la anonymous struct embedding in Go—basically syntax sugar).
>OOP is good for domains that naturally give rise to a clearcut inheritance structure, such as stateful GUI frameworks or ontologies that directly mirror real-world objects

Stateful GUI frameworks are not the only way to build UI frameworks... It'd be pretty easy to argue that people are building stateful UI frameworks because they're predisposed to think about them in terms of OOP.

And so far as I can tell, inheritance ontologies never directly mirror any real world objects. Real world objects are purely compositional with some specialized emergent context-sensitive behaviors.

If you look at more recently developed GUI architectures there is this heavy influence from functional reactive programming. The UIs are modelling as reductions over event streams with no mutable state.

Most architectures around React do this, for example. The reasons why virtual DOM technology has been developed on the web, was to hide away the stateful nature of the older, OOP-influence DOM API.

> OOP is good for domains that naturally give rise to a clearcut inheritance structure, such as stateful GUI frameworks

React begs to differ.

> or ontologies that directly mirror real-world objects.

Such as?

William Cook's response to this is worth a read

http://wcook.blogspot.com/2011/04/paul-graham-on-objects-in-...

"Objects are just collections of first-class functions". " the lambda-calculus was the first object-oriented language: all its data is represented behaviorally as objects"

The lambda calculus is not an object-oriented language, because it does not have hidden (mutable) state. Noticeably, the idea of using 'at-end? and 'read messages goes wrong precisely when there's hidden mutable state behind "stream" (e.g. some other thread consumes the stream in between the 'at-end? and the 'read). As the saying goes, you wanted a banana but what you got was a gorilla holding the banana and the entire jungle. That can't happen in the lambda calculus.
16y later: OOP is still pervasive, but enjoys a healthier relationship with functional primitives in popular languages so that programmers can choose the right tool for each task. Meanwhile, Paul has built an extraordinary career in a role where it makes far more sense to aggressively dismiss established wisdom, creating and coaching new business models. All seems about right!
Conflating popularity with quality is a very common mistake.
and arc is basically dead-in-the-water outside of hackernews
I think these two points are important. From the article:

> Object-oriented programming is popular in big companies, because it suits the way they write software. At big companies, software tends to be written by large (and frequently changing) teams of mediocre programmers.

And from the linked analysis by Jonathan Rees:

> This is related to Lisp being oriented to the solitary hacker and discipline-imposing languages being oriented to social packs, another point you mention.

I would guess that most of the world-changing programming work that can be done is work that requires collaborating with other, unknown people. (But not all! HN itself is a good counterexample, being the work of very few developers and still having a significant influence on the world.) You'll write something at a company, get reorged away (or leave for another job), and someone else will launch it. You'll write some open-source library and years later someone will make something cool with it. You'll contribute to an existing large-scale project, because that gives you leverage you couldn't get from your own project - perhaps because of an inability to replicate the technical accomplishments of the project, but more likely an inability to replicate the network effects of it already having users. You'll build a prototype of something that proves something is doable, but gets significantly rewritten before it goes into production.

If you're interested in these problems, it's in your interest to work in a language that lends itself to other unknown people picking up your work and running with it. If you want to make single-hacker scale projects that will not grow much beyond you, there's nothing wrong with that - but I think you have to be either lucky or able to commit time for the next several years if you want your project to get traction.

Why is there never a spectrum? Why is it always a "single-hacker project" vs. "a huge project with tens or hundreds of contributors"? Why is it always in these discussions that the concept of the functional languages gets hijacked to "they are only used by hackers"? (And what does "hacker" even mean in this context? The word has like 50 meanings already.)

This fabricated extreme dichotomy is killing the discussion. Every time. And it's sad.

We the techies are supposed to be objective and act on a meritocratic basis. I get very disappointed every time I see us just bickering over imaginary strawmen.

I think the dichotomy is from 'pg in this case. I myself like using languages with a balance between the two extremes - Python, being neither the stereotypically enterprisey Java nor the stereotypically write-only Perl, being a good example. They allow me to be productive with single-person projects without overhead, and they're also suited to being used for large-scale projects once you start imposing some structure.

The only thing is you have to be willing to start imposing some structure at some point, and the language has to make that easy, too. It can't be solely suited for the single hacker; it can't have been designed under the assumption that a language for use by large teams of people dispersed over time and space is one designed for "mediocre programmers," and so the things such a language values must be categorically avoided.

> it can't have been designed under the assumption that a language for use by large teams of people dispersed over time and space is one designed for "mediocre programmers,"

I don't know why people find this an offensive statement. EVERY craft on this planet has a pretty small (in terms of percentage) group of people who just grok it and "get it" fantastically well, and everybody else in the profession is pretty average. That's nothing to be offended at: the bell curve distribution is in full effect there. Perfectly normal, no?

And I would argue with you that some languages -- Go being the most modern example -- are specifically designed to be an easy onboard for people with certain backgrouns (C/C++/Java). That Java has been designed to be an easy onboard for people with C/C++ experience is pretty much a documented historical fact at this point as well.

Large companies optimize for quantity, not quality. There's nothing shameful in admitting: "we need a horde of not-amazing programmers who we can treat as interchangeable nuts and bolts"... yet everybody is denying it, when reality clearly shows that's their exact attitude... really makes me wonder why people keep insisting on keeping that circus alive and well (I also heard managers speak of people like that, in expensive saunas and restaurants -- and they were talking quite loudly). Most corporations don't need craftsmen and artisans; they need prentices who build houses with already-made plans.

I am asking for the umpteenth time -- why is this such a bad thing to admit? We cannot all be amazing programmers in each and every way. We have to stop denying this reality.

> I myself like using languages with a balance between the two extremes

Same, that's why I code Elixir: it's FP alright but not "pure" -- has side effects but the edges are clearly documented. And we can't get anything done without side effects anyway. So it's like 95% FP and 5% everything else, in the right places. Perfect.

> I think the dichotomy is from 'pg in this case.

I don't know enough about @pg to claim anything but whatever he said, that's no excuse for everybody else to start gravitating into camps. When people ask me "do you hate Java or do you hate C#?", you know what I say?

"I hate the people who hate programmers based on what they work with."

Because if they admitted it it would hurt public image right?

Also I do not think this is how you build a great company. It's one way to build a company, but it's not a very good way for the long term.

Well I guess their PR would suffer, yeah. It just strikes me as so funny -- at 38 year old -- how so much of our world is pretending you don't do stuff for reasons X and Y while everybody knows you do those things for exactly those reasons. It's absurd and funny.

As for how to build companies, I wouldn't know, not a big business owner. But I am guessing from certain point and on they just want to commoditize stuff and make their management easier and quicker. It all starts with good intentions on paper and always ends up with horrific and dyfunctional teams. I have only seen 3 exceptions for 17 years of career of which 9.5 were spent in offices.

I know Hacker news is hardly the place to disparage anything said by Paul Graham, but I have to ask: is anyone else turned off by his constant reference to "smart people"? Like in the first sentence of this essay, where he immediately declares:

> There is a kind of mania for object-oriented programming at the moment, but some of the smartest programmers I know are some of the least excited about it.

Is that supposed to convince me? "Oh, well, if he knows some smart people who are against it, maybe I should stop liking it, too, because I definitely want to fit in with the smart people Paul Graham knows."

It's such a common thing in Silicon Valley, it kills me. "Always be the dumbest person in the room. Surround yourself with people smarter than you." Like, do I have to ditch any old friends that don't make the cut? What does it mean for someone to be "smarter than me"? Do they have to have a higher SAT score? Do they have to play an instrument better than I do? Do they have to be more financially successful? Can I still learn things from people who are less successful than me?

I just really don't like the mentality of deeming someone "smart" and then using their personal preferences as an inherent argument. That's the fallacy of the argument from authority.

If you don't like OOP, great, carefully explain its shortcomings to me. And perhaps he goes on to do that. I haven't finished reading yet. I just wanted to stop after that first sentence and say that as an opener, I hate it.

However crappy it sounds, there are people who have a hard time grasping relatively simple programming concepts, and then there are also engineers who are astoundingly intelligent and productive.

It doesn't necessarily have to be attributed merely to raw intellect, but denying it isn't in line with reality.

Having worked at many different companies you can definitely see how certain cultures, problem areas and technologies attract people with distinct intellectual bents and how that impacts development velocity.

How much these talented people can leverage their skills is still dependent on the organization, but pretending those differences aren't out there or that you can't hire for these profiles is naive.

I'm not arguing that intellect doesn't vary. I'm arguing against "smart" being the sole measure of who you should spend your time with, and against the act of anonymously name-dropping "smart people" doing something as support for your proposition.
But what would you do? Carry with you a book that has 100 definitions of "smart" and point people at the one you find apt for the current situation?

Not trolling, I am genuinely wondering what would you do. It's not always appropriate to go on huge tangents explaining nuances, especially in casual conversational settings.

You seem to be bothered by the fact that people use shortcut words which carry their own subjective context with them. But that's what natural language is, most of the time -- namely a dictionary compression where the dictionaries of the separate people vary.

It's not really about describing people as "smart." It's about using the argument from authority, and separately it's about the questionable notion that you should only spend time around people who are "smarter" than you.
> it's about the questionable notion that you should only spend time around people who are "smarter" than you

Oh, that I fully agree with. It's a very copy-paste, generic and bland advice that doesn't tell you anything.

It's further made worse by the very obvious observation that everybody is smart in one way and dumb in another. That's what makes communication between humans fun and interesting in the first place.

So yeah, fully agreed.

This is not pg’s best. OOP was invented in academia as a way to scale software design by defining abstractions that you still keep in your head even after it becomes very large. You don’t need OOP to write hello world like many languages such as C# and Java enforces. To pg’s point that hello world becomes quite ugly and verbose. On the other hand I have known C programmers writing million LOC and bragging about never needing OOP features of C++. When I look in to their code I often find that they had just reinvented OOP in their own home brewed way in rather less deciplined and mediocre manner. But still they would consider themselves to be too smart to need OOP.
> But still they would consider themselves to be too smart to need OOP.

Please note that I fully agree with this part; I've met such programmers and then actively avoided them.

But there's always the other side of the coin. You can reinvent your own mini-OOP for a specific scenario you need (recent example: utilize different data parsers depending on a HTTP response header and a top-level XML attribute in the response). And you can make sure you don't suffer from some of the OOP's undeniable problems -- like "brittle superclass".

Ever since I worked with Elixir I had to do something like the above 5 times in total and was VERY happy with my own half-arsed OOP implementation because it allowed me to keep a strict control of things while still giving me the flexibility I wanted for the particular task at hand.