Live data from Hacker News

Inheritance Often Doesn't Make Sense

sicpers.info

131–140 of 255 posts

Re: Inheritance Often Doesn't Make Sense

#131
People in this thread are trying to separate the concept of mutation from the concept of inheritance, but the problem with this is that you can't separate the two. Consider the following pseudocode:

    Rectangle r = new Rectangle(height = 3, width = 5);
    Square s_as_s = new Square(side = 4);
    Rectangle s_as_r = s_as_s;

    print(r.height); // prints 3
    print(r.width);  // prints 5
    print(s_as_s.height); // prints 4
    print(s_as_s.width);  // prints 4
    print(s_as_r.height); // prints 4
    print(s_as_r.width);  // prints 4
    print(r is_a? Square); // prints false
    print(s_as_s is_a? Square); // prints true
    print(s_as_r is_a? Square); // prints true
Okay, so the question comes up when you mutate these results:

    r.height = 2;
    s_as_r.height = 2;
Keep in mind, this is a perfectly reasonable thing to do in both cases: you're just making two rectangles a little shorter. But no matter how you handle this situation, the results are surprising:

One way:

    print(r.height); // prints 2
    print(r.width);  // prints 5
    print(s_as_s.height); // prints 2
    print(s_as_s.width);  // prints 2
    print(s_as_r.height); // prints 2
    print(s_as_r.width);  // prints 2
    print(r is_a? Square); // prints false
    print(s_as_s is_a? Square); // prints true
    print(s_as_r is_a? Square); // prints true
This is the simplest to implement, but only because there's a part of the contract of Rectangle which is implied and not enforced by the compiler. When we change the height of a rectangle, we don't expect the width to change. This is the sort of gotcha that needs to be put in the documentation in big red letters: "WARNING: CHANGING THE HEIGHT MAY CHANGE THE WIDTH IN SOME SITUATIONS."

Another way:

    print(r.height); // prints 2
    print(r.width);  // prints 5
    print(s_as_s.height); // prints 2
    print(s_as_s.width);  // prints 4
    print(s_as_r.height); // prints 2
    print(s_as_r.width);  // prints 4
    print(r is_a? Square); // prints false
    print(s_as_s is_a? Square); // prints true
    print(s_as_r is_a? Square); // prints true
But now you've broken the contract of Square: the user is going to be very surprised when changing the side of one side of a Square means that the Square instance no longer represents a square.

Okay, what about this:

    print(r.height); // prints 2
    print(r.width);  // prints 5
    print(s_as_s.height); // prints 2
    print(s_as_s.width);  // prints 4
    print(s_as_r.height); // prints 2
    print(s_as_r.width);  // prints 4
    print(r is_a? Square); // prints false
    print(s_as_s is_a? Square); // prints false
    print(s_as_r is_a? Square); // prints false
This might be possible with some horrible hack in a language that does dynamic typing. This maintains the contracts of all the types, but I'd argue that it breaks the contract of the language itself: it's deeply confusing to have the type of the s_as_s variable change out from under you.

Perhaps you could do this:

    print(r.height); // prints 2
    print(r.width);  // prints 5
    print(s_as_s.height); // prints 2
    print(s_as_s.width);  // prints 2
    print(s_as_r.height); // prints 2
    print(s_as_r.width);  // prints 4
    print(r is_a? Square); // prints false
    print(s_as_s is_a? Square); // prints true
    print(s_as_r is_a? Square); // prints false
Setting aside how one might even implement this, we've now got the surprising result that s_as_s and s_as_r seem to be different instances now.

Maybe we should have prevent this in the first place:

    r.height = 2; // works
    s_as_s.height = 2; // throws CannotSetSideViaHeightException
    s_as_r.height = 2; // throws CannotSetSideViaHeightException
s_as_s.height = 2 throwing an exception sort of makes sense, but now s_as_r isn't behaving like a rectangle--we're breaking the Rectangle contract again.

Another way to prevent it:

    r.height = 2; // works
    s_as_s.height = 2; // works
    s_as_r.height = 2; // throws ThisWouldBeConfusingException
Again you're breaking the contract of the language, that s_as_s and s_as_r seem to be different objects.

There's only one way left I can think of to make Rectangle maintain its contract, Square maintain its contract, and keep s_as_s and s_as_r behaving the same way:

    r.height = 2; // throws MutationException
    s_as_s.height = 2; // throws MutationException
    s_as_r.height = 2; // throws MutationException
Does this look familiar? It should: it's basically immutability implemented as checks at run time, which is not a good way to implement it. At this point we should just implement these as immutable objects.

I'm not necessarily saying that immutability is the only way. You could also give up inheritance:

    Rectangle makeSquare(int side) {
      return new Rectangle(side, side);
    }

    Rectangle r = new Rectangle(3, 5);
    Rectangle s_as_r = makeSquare(4);

    print(r.isSquare); // prints false
    print(s_as_r.isSquare); // prints true

    r.height = 2;
    s_as_r.height = 2;

    print(r.height); // prints 2
    print(r.width); // prints 5
    print(s_as_r.height); // prints 2
    print(s_as_r.width); // prints 4

    print(r.isSquare); // prints false
    print(s_as_r.isSquare); // prints true
This also results in unsurprising behavior.

Re: Inheritance Often Doesn't Make Sense

#132
post #86
post #77

Earlier quoted context omitted.

This matches my experience: ontological inheritance gets in the way. What we actually want is traits . What can something do , not what it is .

This seems like structural subtyping? Are these synonyms?

The only sub typing in Rust is lifetimes; other types have no sub typing.

Traits are “ad hoc polymorphism”.

Re: Inheritance Often Doesn't Make Sense

#133
post #86
post #77

Earlier quoted context omitted.

This matches my experience: ontological inheritance gets in the way. What we actually want is traits . What can something do , not what it is .

This seems like structural subtyping? Are these synonyms?

More like interfaces. A lot of OOP languages just did it in a limited way where interfaces for a class are closed. Meanwhile in Haskell or Rust, you can define your interfaces, and then provide implementations of it for existing objects like String. Or the Scale workaround, which basically is implicitly converting to wrapper classes that implement the interface. With the goal being extending existing types with new shared behavior, without the limitations of full inheritence.

Re: Inheritance Often Doesn't Make Sense

#134

Earlier quoted context omitted.

I think it's not at all evident that reality is not a value replaced with a new one all the time.

Even if that were the case, it wouldn’t be useful since we experience time with continuity anyways. Bob at time t is still Bob at t+1 even if his state (like position) has changed. If Bob were a value, then he would be another person, we would have to add a persistent ID to the bob values so we could see them as the same object.

> Bob at time t is still Bob at t+1

I guess we're getting more into philosophical issues now. If I leave an ice cube on my counter, at exactly what time is it no longer an ice cube.

Re: Inheritance Often Doesn't Make Sense

#135
post #20

> you cannot use a square everywhere you can use a rectangle (for example, you can’t give it a different width and height) Can someone come up with a better example here? Intuitively, I would say, "Yes, if you ask me for any rectangle, and you reject a square, you are wrong." If you say you can use any rectangle to do your thing, you should absolutely be able to also use a square. Why am I not convinced with the give…

> Why am I not convinced with the given example? Because fundamentally, I don't think "set the width and height" counts as something you can do with a rectangle. A rectangle has a width and height, and you can't just will it to have a different width and height and expect it to obey you through some force of nature. Sure you can. You can both dictate it its width and height at construction time (e.g. the table maker…

The carpenter has certainly altered the dimensions of the board, but in so doing has he not destroyed the old rectangle and created a new one?

Re: Inheritance Often Doesn't Make Sense

#136
post #20

> you cannot use a square everywhere you can use a rectangle (for example, you can’t give it a different width and height) Can someone come up with a better example here? Intuitively, I would say, "Yes, if you ask me for any rectangle, and you reject a square, you are wrong." If you say you can use any rectangle to do your thing, you should absolutely be able to also use a square. Why am I not convinced with the give…

Yes, the problem is that mutable and immutable objects have different methods and therefore fit into different inheritance trees. If you construct an immutable rectangle and pass in the same width and height, you have a square. (An object implementing the same API could be implemented by a subclass whose constructor just takes a width.) If you construct a mutable rectangle with the same width and height, you have a r…

I actually don't think the problem is one of mutability/immutability – it's just that a mutable setting very clearly exposes the problem.

I think the core of the problem is that the object model has permanently and irreversibly associated an identity to an object based on attributes that are actually malleable. Someone else made a good connection to pastry dough, which can be shaped even more freely. We would never hold up a clump of pastry dough, however it is shaped, and proclaim "Down at the core, this is fundamentally a rectangle," because we know the rectangleness is just a temporary description of its spatial attributes, which may change the very instant we accidentally drop the dough and it hits the floor.

Similarly, if "being a rectangle" in our model means "being able to change the width and height independently", then the square should never have been allowed to be a subtype of the rectangle.

I guess this touches more and more closely on what the submitted article discusses, and my initial confusion stemmed from the fact that I have never before heard the "can change width and height independenly" definition of a rectangle, because from my maths background a rectangle is just a description something that happens to be, not something that changes itself.

Re: Inheritance Often Doesn't Make Sense

#137
post #119

Earlier quoted context omitted.

I really dislike this kind of hand-wavy dismissal of ideas/opinions as “programmer-hipster”. It seems to assume the person in question has no intelligent reason behind their thoughts/opinions. And it dismisses the opinion instead of engaging with it intellectually. And having been on the receiving end, it’s insulting. It feels like being called stupid. It’s fine to disagree with opinions, but when you assume people a…

I get what you're saying, but I'm still persuaded this is what happens with Kay and/or smalltalk. 100 times I've seen threads about OO. 99 times someone brought up Kay/smalltalk. Zero times did they specify why the messaging model was relevant to the present discussion. That's important to do, because more OO programmers use C++/C#/Java, and are not smalltalk experts. So, it seemed to me that they were namedropping.…

Duck typing is central to the Smalltalk "vision thing". That and the idea that a message is a symbolic representation of a method, which an object may or may not support. It is not, in fact, the method, nor does it stand for a direct call to the method. In C++ and Java, for a call to a.foo(12) the compiler can look up the class of a, determine whether it has a foo implementation, and if so compile a call directly with the number 12. In Smalltalk, a foo: 12 has no such implications. Rather the runtime will look up whether a's class has a foo: method at call time and if so, invoke that method; otherwise invoke the object's doesNotUnderstand: method with the message and arguments as parameters. There's an added layer of indirection there. It's rather like a single-threaded Actor model. Kay was trying to get people to think in terms of encapsulated objects that communicate in (often ad hoc!) ways, not in terms of inheritance trees or type hierarchies.

Is it better? Is it worse? I don't know. We know Smalltalk is slower, but it (along with Objective-C) admits designs unfathomable in C++. For example, all objects that understand a given protocol (set of methods) and implement it in a sensible way are substitutable with each other (with respect to the protocol) irrespective of their place on the inheritance hierarchy (or even whether such protocol was formally defined). This is NOT true with C++, absent template metaprogramming, and it's not true with Java unless you define an interface and assert that all classes which understand those methods implement that interface.

Furthermore, by overriding doesNotUnderstand:, you can specify a behavior for classes when sent a message they don't have a method for, besides raising an error. Maybe you want them to forward the method to one or more delegate objects, or log the invocation.

It's super-flexible, powerful, and neat in a similar dynamic-language way to how Lisp is neat. That's why Smalltalk attracts so many fervent converts. As I said, it can be better or worse depending on your perspective. Some people like working with type hierarchies; Haskell, Rust, and even C++ attract fervent converts too!

Re: Inheritance Often Doesn't Make Sense

#138

Earlier quoted context omitted.

Even if that were the case, it wouldn’t be useful since we experience time with continuity anyways. Bob at time t is still Bob at t+1 even if his state (like position) has changed. If Bob were a value, then he would be another person, we would have to add a persistent ID to the bob values so we could see them as the same object.

> Bob at time t is still Bob at t+1 I guess we're getting more into philosophical issues now. If I leave an ice cube on my counter, at exactly what time is it no longer an ice cube.

Mutability is mutability, it also applies to ontology even if most OO languages don’t model dynamic ontology with inheritance (unlike say Self or Cecil).

Your ice cube was never just an ice cube in the first place, it was just some water that happened to be frozen as a cube...once heat was applied to the water, it’s state changed so that it eventually could no longer be classified as an ice cube.

Re: Inheritance Often Doesn't Make Sense

#139
post #96

The programmer's perspective: The phrase 'object-oriented' means a lot of things. Half are obvious, and the other half are mistakes. - Paul Graham. Implementation inheritance causes the same intertwining and brittleness that have been observed when goto statements are overused. As a result, OO systems often suffer from complexity and lack of reuse. - John Ousterhout Scripting, IEEE Computer, March 1998. The problem w…

> The conceptual purist perspective: > > The notion of object oriented programming is completely misunderstood. It's not about objects and classes, it's all about messages. - Alan Kay > Given that Alan Kay is the one who came up with the term "object oriented", it might be wise to give him authority about the definition.

I think this means doing composition/delegation, which the author of TFA calls "basically giving up on implementation inheritance."

Re: Inheritance Often Doesn't Make Sense

#140
post #72
post #20

> you cannot use a square everywhere you can use a rectangle (for example, you can’t give it a different width and height) Can someone come up with a better example here? Intuitively, I would say, "Yes, if you ask me for any rectangle, and you reject a square, you are wrong." If you say you can use any rectangle to do your thing, you should absolutely be able to also use a square. Why am I not convinced with the give…

> if you ask me for any rectangle, and you reject a square, you are wrong No, no: "If you ask me for any object that can have independent width and height, and you reject a Square, you are..." right, of course. It depends on the properties that define what a Rectangle and a Square are in your code.

> No, no: "If you ask me for any object that can have independent width and height, and you reject a Square, you are..." right, of course. It depends on the properties that define what a Rectangle and a Square are in your code.

I think you nailed an important distinction here, but maybe used the wrong words in doing so. The correct definition of a rectangle, with this interpretation, is any object that can change its width and height independently.

I'm just not sure that this is such a sensible definition of a rectangle.

In my mind, the word "rectangle" is a description of how things are in this instant, not a description of in which ways something can change in the future. The latter is a valid concept and something we need a word for, but I'm not sure "rectangle" is such a good candidate.

This also opens up for a solution to the problem:

- In the first sense, rectangle > square. A rectangle has four straight angles, and squares are rectangles where both lenghts happen to be the same.

- In the second sense, square > rectangle. Squares are things which can be scaled, and rectangles are squares which can scale each axis independently.

Post reply on HN