Live data from Hacker News

Joe Armstrong: Why OO Sucks

harmful.cat-v.org

121–130 of 267 posts

Re: Joe Armstrong: Why OO Sucks

#121
post #74

Earlier quoted context omitted.

Instead of giving you an example that anybody actually uses, I'm going to tell you about a cool idea I've been reading about that hasn't gotten much actual use. The basic idea is to use a generalization of pattern matching. Languages like ML and Haskell support pattern matching, but in rather limited ways. Crucially, patterns are not first-class citizens of the language. (For Haskell, at least, there are some librari…

Thanks for an excellent write up to the idea. That was very clear. I am very intrigued and was looking at purchasing that book to learn more - but then I saw the price. I'll have to scrounge a copy from a library somewhere. I thought on demand printing was going to reduce the costs of books on the long tail!

Oh yeah, it's more expensive than I thought :/. Happily, I was lent it by a friend.

You might have some luck getting it as an ebook from the library. My university's library seems to only have it as an "electronic resource", and it's a pretty big library.

I like the book, but a good part of it is just background. It goes through a bunch of variations on the lambda calculus in building up to the "pattern calculus". If you're already familiar with the basics, you might be better off just reading some papers on it. (Hopefully there are some you can read for free, but I'm not sure.)

Re: Joe Armstrong: Why OO Sucks

#122
post #54

Wow, I am genuinely shocked by the comments in this thread. I didn't realise that so many people held the polar opposite view to me. It's a bit like suddenly finding out that all your friends are racist. I love object oriented programming. For me it aligns perfectly with the way I think - it allows me to produce a system of interrelated 'things' where each thing (or group of things) has a well-defined role and can hi…

So what is the natural and clean design for an operation that represents the sale of a property? Say we have these objects: the buyer, the seller, the agent, the property and the contract. Which one of these would you prefer? property.sell(buyer, seller, agent, contract) seller.sell(property, buyer, agent, contract) buyer.buy(property, seller, agent, contract) agent.sell(property, buyer, seller, contract) contract.si…

  contract = agent.makeContract(property, seller, buyer)
  seller.signContract(contract)
  buyer.signContract(contract)
  agent.registerSale(contract)
Nothing complicated here, really. Just follow the "natural" flow and respect each actor's role. Don't try to group all operations into a single one when there are independant actors involved.

Note : I see the "contract.sign()" solution coming back a lot. I will admit this is "pure OO", but to me it doesn't make any sense. A contract here is a data object, not an actor. It shouldn't process anything. How many times do you see contracts sign themselves (or even do anything) in reality?

Re: Joe Armstrong: Why OO Sucks

#123

Earlier quoted context omitted.

So what is the natural and clean design for an operation that represents the sale of a property? Say we have these objects: the buyer, the seller, the agent, the property and the contract. Which one of these would you prefer? property.sell(buyer, seller, agent, contract) seller.sell(property, buyer, agent, contract) buyer.buy(property, seller, agent, contract) agent.sell(property, buyer, seller, contract) contract.si…

Which one is best: cats, alligators or pencils? You're question is about how best to model a domain of which you've given the scantest of details. Domain modelling is something you have to do in any programming paradigm and is equally as obtuse if you don't have proper knowledge of the domain. Come back with a more detailed description of the domain and then we can start comparing the emergant designs from OO and oth…

The point is, if several objects are involved, all of them get mutated and all of them are in a type hierarchy, you're going to have a hard time justifying which object should be the receiver of the message based on what feels more natural.

Re: Joe Armstrong: Why OO Sucks

#124

I've been watching some talks online recently by Rich Hickey of Clojure fame, and he's a very interesting and convincing speaker. He basically makes the same argument that Armstrong makes here. I'm not clear, however, how the pro-FP, anti-OO crowd address the Law of Demeter, which is often summarized as "One dot: good. Two dots: bad." The canonical example where the Law of Demeter serves us well comes from some of th…

So, there have been a few replies to this post, but as far as I can tell, you're asking about maintaining invariants ("not breaking the structure"), which nobody has addressed so far. Let's have a look how this is handled in different types of languages (^ means I have extensive experience with these, so I might have some of those without a ^ wrong - some languages are also tough to categorise, but bear with me):

1. statically-typed OOP: e.g. C#^, Java^, C++^, F#, OCaml, Scala, etc.; specifying the type of a field is a kind of invariant assertion (e.g. name must be a string not a number), and depending on the expressiveness of the type system, these static assertions can become arbitrarily specific. You might still need to do runtime checks in the mutating methods if certain invariants (e.g. length > 0) can't be expressed statically in the language. If internal state can be set to mutable objects (e.g. mutable strings) from outside, you can still break the invariants by mutating the objects after they've been checked. Some type systems (such as Java's generic types) are even notoriously weak, requiring extra care.

2. dynamically-typed OOP: e.g. JavaScript^, Objective-C^(see note), Ruby, Python, Io, etc.; generally, no invariants are maintained by default, and everything happens at runtime. So unless you specifically check invariants in the object's methods, invariants can often easily be broken by passing in data with unexpected structure. It is often trivial to pass in objects that satisfy the invariants, then change them in arbitrary ways to violate them afterwards. Encapsulation is frequently also easy to break by extending classes or prototypes.

3. statically-typed functional or procedural: C^, Haskell, ML, ATS, etc.; just as with the statically-typed OOPLs, you can explicitly encode many invariants in the type system. You can't set a number-typed field in a record to a string value. To maintain "softer" invariants, you will typically have runtime checks in the mutating functions. The languages with a pure functional bent typically encourage concentrating mutable state into a select few places in the code. Those places can be gatekeepers for enforcing invariants.

4. dynamically-typed functional or procedural: Lisps such as Clojure^, Scheme and CL^, Erlang, etc.; Where mutable state is allowed, or the default it's just as much a free-for-all as the dynamic OOPLs. You need to make your mutating functions aware of all your invariants, or they're trivially easy to break. In the pure-functional or immutable-by-default languages, again, since mutation is concentrated to a few hotspots (transactions in Clojure, messages in Erlang), those hotspots can easily be made to refuse performing mutations that break invariants.

Comparing 1 with 2, and 3 with 4 respectively, it's pretty obvious that the static languages make it easier to maintain invariants. The more expressive the type system, the more you can nail down the permitted structure. The culmination of this are theorem-proving languages. Interestingly, those tend to be functional.

So, if you compare 1 and 3, encapsulation buys you some safety in some cases, but realistically, it's just as easy to make a system that permits violation of invariants as it is in functional languages. Some of the more pure functional languages replace encapsulation with constrained mutability to help maintain invariants.

Comparing 2 and 4, I'd argue the functional languages actually do a better job helping you maintain invariants. The malleability of objects in (2) languages makes them hard nail down. Immutability on the other hand at least provides stability in time, if not in structure.

Note that I'm only evaluating the ability to maintain invariants here, not trying to come up with an overall judgement on languages. In some situations, maintaining invariants is very, very useful. In others, you don't really care that much.

(note on Objective-C): while you declare fields and variables with a specific static type, any object-typed field can point to an object of any run-time type. So the static types are mostly just annotations and affect ARC semantics, they're not actually enforced or safe, it basically relies on duck typing; Key-Value Observing in particular makes some pretty deep changes to the run-time types of objects, which wouldn't be possible in e.g. Java. Unlike some of the other dynamic languages, the non-object C types and mostly-fixed memory layout of objects give some level of structural safety. Still, there's nothing stopping you from creating a number class that returns YES for isKindOfClass:[NSString class], thus making it straightforward to fool most invariant checks.

Re: Joe Armstrong: Why OO Sucks

#125
post #114
post #74

Earlier quoted context omitted.

Instead of giving you an example that anybody actually uses, I'm going to tell you about a cool idea I've been reading about that hasn't gotten much actual use. The basic idea is to use a generalization of pattern matching. Languages like ML and Haskell support pattern matching, but in rather limited ways. Crucially, patterns are not first-class citizens of the language. (For Haskell, at least, there are some librari…

This is the first time I've encountered this and what you wrote packs a lot of ideas in a small space so forgive me if I have misunderstood. What you write seems like an even more powerful version of the Active Patterns in F#, which are already really powerful. Active Patterns are the closest thing to First class patterns in a production language * . Haskell has a similar thing in views. But I think another concept,…

I'm afraid that I'm not too familiar with either active patterns or GADTs at the moment. However, given the understanding that I do have, I think they are both unrelated to the basic dynamic pattern matching that I was talking about.

As far as I can tell, active patterns only affect the constructor. They let a constructor like Even to run an arbitrary function when it's matched. This lets you customize what a constructor actually means, which seems very useful, but is orthogonal to dynamic patterns. Dynamic patterns affect the patterns and not the constructors, where a pattern is the expression that is actually matched. That is, given a function f (Foo a b) the pattern is Foo a b. Active patterns would let me customize how Foo works, but the full pattern (Foo a b) still looks and acts the same.

Given this meaning of "pattern", I think active patterns do not constitute first-class patterns. You can't take a pattern and pass it into a function or make up a pattern of variables that can be any pattern. With first-class patterns, you should be able to take the pattern (Foo a b) and pass it into a function as a pattern. So a function like

    match pattern (pattern x̂) = x
would make sense in a system with first-class patterns. Here, a pattern is an argument to a function and then used to match against the next argument. Hopefully this clarifies the particular thing that dynamic patterns add to the language.

GADTs are also unrelated. For one, GADTs only affect the type of a constructor and not its behavior. I did not talk about types in my post at all--everything I talked about would make sense in a dynamically typed language that had pattern matching. It just so happens that dynamically typed languages tend not to have pattern matching like this, so it's associated with statically typed languages.

Also, GADTs, like active patterns, only affect the constructor. They do not change the actual patterns used to match against values (except by restricting possible types of the matched values).

So really both active patterns and GADTs are somewhat orthogonal to dynamic patterns. When I said a code that is generic over the head of the pattern, I meant a function like:

    doFoo (a b) = b
where doFoo will match both Foo x and Bar x and Baz x and extract x in every case. This function is generic over the constructor in the sense that it matches regardless of what the constructor actually is--the constructor becomes just another matchable term.

This sort of function, which just uses static patterns, is already something that a GADT cannot do. Dynamic patterns are even more flexible. My match function above lets you take an arbitrary pattern like (Foo a b) and match (Foo a b x) against some value to get x. You're passing a pattern into a function which then uses that pattern to match against its next argument. This feature has nothing to do with the type of the constructor.

I hope this cleared things up for you. However, it's almost 4 in the morning and I'm not entirely coherent. If you're still confused, or if I made some glaring errors, feel free to email me about this when I'm more awake :P.

Re: Joe Armstrong: Why OO Sucks

#126
post #122

Earlier quoted context omitted.

So what is the natural and clean design for an operation that represents the sale of a property? Say we have these objects: the buyer, the seller, the agent, the property and the contract. Which one of these would you prefer? property.sell(buyer, seller, agent, contract) seller.sell(property, buyer, agent, contract) buyer.buy(property, seller, agent, contract) agent.sell(property, buyer, seller, contract) contract.si…

contract = agent.makeContract(property, seller, buyer) seller.signContract(contract) buyer.signContract(contract) agent.registerSale(contract) Nothing complicated here, really. Just follow the "natural" flow and respect each actor's role. Don't try to group all operations into a single one when there are independant actors involved. Note : I see the "contract.sign()" solution coming back a lot. I will admit this is "…

Your "data object" versus "actor" argument is intersting. Does that mean an object model should mimic certain properties of the real world that aren't even part of the system, like the knowledge that agents can act whereas contracts can not?

There are clearly conflicting goals here. It could be that you need polymorphism along one hierarchy but that would make the design look very unnatural in the eyes someone who knows the problem domain.

In my experience, OO models tend to diverge greatly from the real world over time, because after all we're not modelling the world, we're modelling a solution to a problem and we shouldn't fight the tendency for the language of the solution domain to dominate the language of the problem domain.

Sometimes this can mitigated by having interfaces on different levels of abstraction, but that often leads to bloated and slow systems.

Re: Joe Armstrong: Why OO Sucks

#127
post #118

Earlier quoted context omitted.

So what is the natural and clean design for an operation that represents the sale of a property? Say we have these objects: the buyer, the seller, the agent, the property and the contract. Which one of these would you prefer? property.sell(buyer, seller, agent, contract) seller.sell(property, buyer, agent, contract) buyer.buy(property, seller, agent, contract) agent.sell(property, buyer, seller, contract) contract.si…

To a certain extent it may depend on context (eg code for a conveyancing firm would have a different focus to that of a landlord or mortgage company), but a lot of those instances would already be properties of other instances. In particular, since the contract is the object which ties them all together, I would imagine the best syntax would be closer to: contract = new Contract(property, seller, buyer, agent) contra…

That could be a useful solution (and the one I would probably choose as well), but what if you primarily need polymorphism along the property type hierarchy because the sale of a home is so different from the sale of a mall?

Also, you get the objection that contracts don't sign themselves. I remember very well that in the early 90s, OO models were promoted as a means for business people to talk to software designers. It never worked out that way.

The real world knows processes. Processes are not some appendage of any of the objects involved. So why not model a process as a function?

sell_property(contract, property, buyer, seller, agent)

Re: Joe Armstrong: Why OO Sucks

#128
post #112

Earlier quoted context omitted.

So what is the natural and clean design for an operation that represents the sale of a property? Say we have these objects: the buyer, the seller, the agent, the property and the contract. Which one of these would you prefer? property.sell(buyer, seller, agent, contract) seller.sell(property, buyer, agent, contract) buyer.buy(property, seller, agent, contract) agent.sell(property, buyer, seller, contract) contract.si…

Without knowing any more about the system I'd suggest: contract.sign(property, buyer, seller, agent) It seems that properties, buyers, sellers and agents could exist without 'knowing' about contracts, but a contract at some point needs to reference the other entities. Also, it seems odd that the contract can exist without knowing the details of buyer, seller, etc. I'd prefer: new Contract(property, buyer, seller, age…

I would tend towards this solution as well, but does it meet the "looks natural" criteria? Contracts that sign themselves?

I think many classes we invent are nothing more than processes in disguise and we could just as well model them as functions.

A simpler example. Should the BankAccount class have a transferTo(BankAccount other) method to transfer money into another account? What if there are different account types and the exact process depends on the types of both accounts?

Of course it's possible to do it that way, but is it really the cleanest way to imagine this? I don't think so.

Re: Joe Armstrong: Why OO Sucks

#129

I've been watching some talks online recently by Rich Hickey of Clojure fame, and he's a very interesting and convincing speaker. He basically makes the same argument that Armstrong makes here. I'm not clear, however, how the pro-FP, anti-OO crowd address the Law of Demeter, which is often summarized as "One dot: good. Two dots: bad." The canonical example where the Law of Demeter serves us well comes from some of th…

Umm... Pseudo-code follows

     module Book = struct 
         type t = { sections: Section.t list; ... }
         ....
         get_paragraphs t = map (fun s -> Section.get_paragraphs s) t.sections  
         .....
     end
So it is not that hard to have both FP and DL

Re: Joe Armstrong: Why OO Sucks

#130

A series of assertions and "I just don't see it"-s presented as self-evident when they are anything but. No examples of real cases where OO does in fact "suck", ending with the claim that in order to understand the popularity of OO one should "follow the money". What? I mean, I don't even...

Indeed, it's a very odd piece. No arguments except opinions ("state is bad", "functions and data shouldn't mix" etc). I fail to see the underlying reasoning for any of his gripes. It just comes of as a frustrated computer scientist / mathematician complaining that his theoretically correct way of making machines do his bidding, is not loved by all.
Post reply on HN