A couple of other comments have argued that "ontological inheritance" and "abstract data type inheritance" are actually the same thing:
> This is because Squares are Liskov substitutable for Rectangles... which is because Squares are, platonically, a kind of Rectangle.
> If the type system is sound and expressive enough, ontological inheritance ( this thing is a specific variety of that thing) and abstract data type inheritance (this thing behaves in all the ways that thing does and has this behaviour) should be essentially the same thing.
The difference is that ontology exists in the mind of the programmer, but Liskov substitutability is a property of the program itself. No matter how you model it, a Square is "platonically a kind of" Rectangle. But in order for them to be Liskov substitutable, you have to model them in compatible ways. If my Square class only has a sideLength method, I can't substitute it for a Rectangle.
This simple example may seem silly, but as models get more complex, it becomes harder to make them compatible, even if one modeled class of objects seems like "platonically a kind of" another class. You see this kind of thing all the time in real-world systems. For example, in a UI system, an "OpenGL View" is conceptually a kind of "View", and this relationship is modeled by making OpenGLView a subclass of View. Normal 2D drawing doesn't work in this kind of view, however, so it's not Liskov substitutable.
I always find that the common example of Rectangles and Squares leads me to a different conclusion. It assume that the best way to go about is to only have a single sideLength method, but from a data structure perspective it seems more obvious that the Squares class substitute instead the validation method by adding constraints that a valid square only exist when both sides are equal. Rectangle class must already have a validation method that check that each side is greater than zero, so it seem like the obvious place to define a square. Any additional methods or properties like sideLength would just be added convenience and optimization to access the width and height at the same time, but which is not required for anyone substituting one for the other.
But then both are just a kind of polygon, with an arbitrary number of sides of arbitrary length.
At which point you really want a variable length list of Sides, each with a length property.
Or you want a series of points with relative coordinates, making the sides implicit.
Or or or...
What I take from it is that there is no one true object model, there is no universally "correct" way of solving the problem. An evolving understanding of the problem leads to an evolving solution that places different priorities on different attributes.
Is there any value provided to the system by making Square a specialisation of Rectangle? Is there value in being able to express a Square without a redundant attribute value? What is the tradeoff? And so on.
It's interesting because it highlights that if a thing has so many properties describing what it is, then at a certain point it's not a thing at all, but a collection of characteristics that could be tweaked to describe anything.
At that point, the idea of behaviors is rendered void, as the universe of possible behaviors is just too large.
So, I think that's the guiding principle in your suggestion to consider what value a given design provides to the system: that is, start by modelling the behaviors your system requires, then create an object hiearchy that reflects those behaviors most concisely.
This is probably our intent, but we may be derailed by too little respect for YAGNI.
This is a very insightful point. These models and concepts are all human created abstractions.
They contain elements of arbitrariness, describe different levels of detail, focus in on certain aspects or dynamic, leave out other information, and so on.
The skilled software developer must determine how useful and well-suited any particular abstraction is for the actual problem at hand, and judiciously move forward that.
As understanding of the problem domain grows, the abstractions should be updated and discarded to reflect this.
Pragmatism is key -- what does the system actually need to do? Supposed platonic ideals of some conceptual object are often a wild goose chase. Reality is complex. We must embrace it.
The problem is that the square is a rectangle in the mathematical sense: a rectangle with ANY additional constraints while code which uses a rectangle expects a rectangle with NO further constraints.
I believe the right choice is to have an AbstractRectangle class representing the rectangle with any further constraints and Rectangle class representing the rectangle with no further constraints.
As niklasjansson points out, this solution breaks Liskov substitution. Squares programmed in such a way are no longer substitutable for rectangles. Breaking Liskov substitution is a great way to hide bugs and make your codebase extremely hard to reason about, due to the fact that every subtype can have drastically different behavior than what's defined by the parent type.
Yes! The thing that ends up breaking in the squares and rectangles examples is mutability. Remember that math things are immutable by default. This thing is a square, and by definition also a rectangle, and since its properties and identity are immutable, that will always be true.
However, programming takes those immutable concepts and tends to make them mutable. So now we have a rectangle, and we can change its identity by, say, scaling its width. And it's obvious that scaling just the width of a square will make it no longer a square. So now a square cannot support the same operations that a rectangle can. So now a square is no longer Liskov-substitutable for a rectangle.
Absolutely, and that's another excellent way to explain the problem and come to the same conclusion. The mathematical relationship between rectangles and squares is covariant, because squares have more specific constraints than rectangles. But when you make rectangles mutable, those mutations are contravariant, because they only guarantee to preserve less specific constraints. So you can't make squares from mutable rectangles!
Excellent point! I think it's fair to say that substitutability can only be satisfied (in general) for a fixed set of operations. This is why I believe Haskell's (and by extension Rust's) approach to bounded ad-hoc polymorphism is the best that I've seen in a production language. Going beyond Haskell, Rust also allows you to choose static dispatch, making bounded polymorphism zero cost. Dynamic dispatch is still available, when desired or needed (but it's almost never needed).
I'm a huge fan of the Rust and Haskell systems for exactly this reason. I sometimes play with the idea of a language that has that kind of type system, but with a more forgiving environment Rust or Haskell. Something like C#, but swap out the type system to feel more like Rust.
> The thing that ends up breaking in the squares and rectangles examples is mutability.
Mutability isn't sufficient.
Breakage requires more than mutability. In dynamic languages, a mutated object can retract its squareness (smalltalk `become`; javascript __proto__ assignment; python object self-mutation). And predicate types (it's a Square iff its sides are the same length) are even graceful.
So I'd rephrase that as "if a specific implementation can mutate in ways that invalidate the laws expected of it, and you can't mutate the laws expected of it to match, your implementation might not match expectations". "Might not", because relaxed and pruned expectations may be locally sufficient.
Good point. I would expect the immutable version of scaling a side to return a new rectangle, thus allowing a scaled square to also return a rectangle. I hadn't thought about mechanisms that would allow a type to basically upcast itself in a mutable operation. Of course, for typing proposes, the signature looks the same: This operation yields a rectangle. It's really just a matter of where the return value lives.
> for typing proposes, the signature looks the same: This operation yields a rectangle. It's really just a matter of where the return value lives.
Well, it yields a type union of square and rectangle. A runtime, whether the original language appears mutable or not, can play games with that. Like resolving types lazily. If for example some object is only drawn, and draw happens to dispatch it independently of squareness, on say a predicate type DistantObjectThatLooksLikeADot, then there's no need to resolve whether it was square. No one will ever know. Or ever have to pay the cost of finding out. It has say a MightBeSquareMightBeRectangleHaventHadToCareYet dictionary. :) Which can become important as types get more expressive, and expensive to prove/test.
Sum types! I don't get to use that kind of stuff very much, so I tend not to think about them off the top of my head :)
How do languages with sum types handle scenarios where the type is A+B, but B is a subtype of A? So you're really guaranteed to have an A, but you may or may not have a B? Do they allow transparent reference as type A, or must you disambiguate first?
That is, given a function that takes an A, can you pass it a type A+B, given that B is a subtype of A?
This is far from a silly example; it gets right to the core of the matter. As belorn mentioned, a square is a constrained rectangle, and inheritance by extension cannot represent this relationship.
> square is a constrained rectangle, and inheritance by extension cannot represent this relationship
A hypothetical nice type system, providing a push-out (path independent) lattice of theories/algebras (eg triples of types, operators, and laws) can represent this relationship by extension. It's just adding a law.
That we don't have such a type system available, is I suggest, perhaps the most crippling characteristic of our current tooling. But given the levels of type system research funding over the last decades, it's rather a self-inflicted injury. :/
Agreed, though I feel that phrasing it as 'adding a law' to include it under the umbrella of 'extension' tends to diminish, or distract from, the problem with inheritance as implemented in current mainstream languages. What you are describing seems to be a fundamentally different way of looking at the issue.
Fair. It seemed worth mentioning the larger context, and this seemed a plausible place.
Doing VR/AR, I've been struck by how not good the community is at attributing design constraints to cause. Constraints are simply echoed ("90 fps or sick!"), without attention to their context and scope of validity. Being unable to clearly see one's design space, impoverishes imagination, planning, and outcomes.
Many of the comments on this page reminded me of that. And parts of the OP. We can usefully discuss whether the apocryphal shoemaker had better today prohibit his oft-crippled children from dancing, or going outside, or walking, but it's worth bearing in mind that the root cause of the cripplings is the ongoing failure to provide shoes. And all the rusty bent nails we've left on the floor.
Inheritance can represent this relationship, as long as all the defined operations in the rectangle are covariant. Because squares are covariant (more constrained) than rectangles. This generally means that the rectangle type must be immutable.
Indeed, but unfortunately these complications almost never comes up when programming in current mainstream object-oriented languages is taught. Usually, the take-home lesson is, "if you have a type hierarchy, use inheritance - it's simple."
You are absolutely right that there is an education problem. I got lucky by randomly deciding to take the OOP principles elective. It's still not a required course at my school, though I know from recruiting trips that other schools include it.
I think there's a place for a language that separates inheritance from subtyping. A lot of misuse of inheritance that I see comes from inheriting for functionality instead of subtyping.
> but as models get more complex, it becomes harder to make them compatible
One thing that helps, is locally-scoped facades, like ruby's refinements.[1] You don't need complete model substitutability, only locally-sufficient substitutability.
From a write-only perspective. If you imagine mutating operations that square might have (for example setSideLength) rectangles can satisfy them perfectly well.
If you ever open the hood of a modern car and look around a bit you'll discover that at the front there's a half dozen pullies connected to one or more belts. Each of these pullies is connected to some diffeerent system each able to turn rotational acceleration into a useful service. The alternator converts that rotational energy into electricity. The AC unit uses it to operate a heat pump. The coolant and oil pumps use it to circulate fluid. In the past, these parts were sometimes driven by another system or integrated directly into another part. But over time, as a convenience to many different parties it was decided they should all implement a pully and connect dirctly to one of the main belts that connect directly to the motor. Not a single one of these parts has anything you'd call a taxonomical relationship. They do have an invented one. And it is as much about conveniencing the overall system design as much as conveniencing the individual part design. They are all "pully implementers" subscribed to the "belt druve" system.
This is the inheritence I find most powerful in software. These things aren't the same but if we treat them the same, the downstream and upstream code are simpler, easier to replace and consistent. I don't worry too much about the philisophical relationship between the objects.
In the language of the article, this is subtyping without subclassing. Sure Java/C# implement this as interfaces, but in C++ this would be a case of multiple inheritance. This is a good example of the article's main point that people mean a lot of different things by inheritance and get different value from the different applications of the concept.
Structures of function pointers and, what, string method names? My C++ is rusty, but that's all I can think of. I guess there's a lot of variations on structures of pointers...
Templates and compile-time polymorphism. This is what the STL is based upon. A forward iterator is any type for which the * and ++ operators are defined; you can use it in any template that makes use of just those syntactic forms, but will get a compiler error if you try to instantiate a template with a type that does not support those operators.
Punning on memory layouts, a la PyObject_HEAD or some of the objects in V8. With this approach, you make sure that all related types have the same memory layout for a subset of fields at the beginning of the struct. Then, a reinterpret_cast on a pointer to one member of the type will yield the same values as a different member of the type, and so can be used generically by code that depends only upon those members. (You have to be very careful with this approach, because multiple inheritance and vtables will usually break it, but if you absolutely positively must have zero-overhead runtime polymorphism it's occasionally the best option.)
#include and preprocessor magic to redefine symbols based on preprocessor defines. Used to be the old way to swap out different implementations of a cross-platform interface, but seems to have largely fallen out of favor in the last couple decades.
Linker magic to accomplish the same, where the header file defines a common interface but the linker may link with one of several different object files to implement that interface at link time. This one is still fairly commonly used; Google and Facebook both have their own custom STL & libstdc++ implementations, and this is also commonly used to swap out eg. malloc implementations.
dlopen & dlsym to do this at runtime. Header file defines the interface, dlsym gives you back a function pointer or set thereof for a plugin implementation.
In Sean Parent's 'Inheritance is the base class of evil' talk Sean outlines an interesting way to achieve runtime polymorphism without inheriting interfaces in C++.
I think the term “interface” is used in many related but different ways in software, but I think the most obvious mechanical analogy would be basically anything that uses a common “plug” or “adapter.” Like a vacuum cleaner with different attachments. Or even HDMI for video/audio.
You're confusing inheritance with implementing an interface. I'm not sure there is an example in the real world, because the real world is concerned with function, not taxonomy. It hardly makes sense to say "cordless drill inherits from screw driver"; rather, they both implement the "drives screw" interface. Ontological inheritance really doesn't make any sense at all for modeling systems (sometimes ontologies are useful as data in a system, but probably never for modeling the system itself).
They wanted an example of inheritance. Something like:
You need an office. You could create a new office from scratch, or you could also just put a desk in a room in your house and give it a unique address like "Suite 1". Now it's both a Room and a HomeOffice.
But if your HomeOffice exposes a method BuildWall(), which mutates the office into a collection of two Rooms, then it would stop fitting the definition of a Room after calling that method.
I don't know if there are different tax consequences for one-room vs. multi-room home offices, but that's one area where the distinction could at least conceivably be significant enough to affect whether you'd prefer to build a separate office or inherit the old room, or maybe just rent an existing office from a third party.
Oh rats, my mistake. It is harder to come up with s good example of mechanical inheritance. Something like the cassette tape adapter kinda feels like inheritance, in the sense that it can be substituted in for a normal cassette tape but provides its own unique functionality.
Yes, I'm struggling to think of a good mechanical example of inheritance as code reuse.
But I don't agree with the "that's an interface, not inheritance" argument. Well designed super classes tend to provide an interface - they just don't use the "interface" keyword.
If you declare a method as abstract, you're defining an interface that subclasses use to define new behaviour. You see this all the time. If a method is defined as protected, you're saying "I'm allowing subclasses to use this interface to change behaviour".
For example, test frameworks usually provide an overridable method that's run before each test - setUp. That's interface - "I will call setUp before every test - feel free to override it".
To me, that's where shit gets hard. It has to have a well defined interface, it has to be an is-a relationship and it has to be a subtype (Liskov). That's why we see composition over inheritance because it's far simpler to just drop one of the constraints and get decent work done.
What you are describing is an interface, not inheritance. They are implementing an pulley interface, not inheriting a specific pulley implementation from a common housing or something.
The distinction between "implementing" an interface and "inheriting" a base class is largely just a Java-ism.
To some extent, it's a distinction without a difference - at a semantic level, Java only distinguishes between interfaces and purely abstract base classes because one allows multiple inheritance and the other doesn't, and the separate "inherits" and "implements" keywords flow from there. Other languages (like C# and Kotlin) distinguish between interfaces and overridable classes for the purposes of deciding when to allow multiple inheritance, but use the same syntax and terminology for inheriting from both interfaces and base classes. Still other languages make no structural distinction, but still use the word "interface" to refer to base classes that don't include any implementation, and therefore aren't so susceptible to the diamond inheritance problem. TBH, I think the Java-style treatment is the least conceptually consistent, since it's entirely possible to "inherit" a base class without inheriting a single bit of information, either because everything it declares is abstract or because everything got overridden.
There's a pretty decent coverage of all the grotty details of this concept space, and how different languages treat it, in the beginning of the gang of four book.
I kind of like interfaces as they are basically less powerful traits. I think there is a big difference to class inheritance and the difference between e.g. Jave and C# is just syntactic.
When you implement interfaces you have to explicitly implement all methods. If you implement them correctly all methods using them continue to stay correct (including C# extension methods or java interface default methods).
With class inheritance you have two large problems: you also inherit state that you might not need, might not have access too and might change in future versions of your base class.
Secondly you might need to override methods of the base class to provide correctness. If your base class adds/changes methods your subclass can easily behave wrongly.
Never looked at it that way. With inheritance, you're essentially re-using behavior that treats like things with like properties in a similar way. With interfaces, you are providing unique, black-boxed behavior that conforms to an operational specification.
Interfaces are also more externalized. They are intended for consumption from outside of any particular class hierarchy, and don't necessarily represent like things nor operate on the internal properties of a particular class or hierarchy.
OTOH, consider the use of inheritance wherein a major operation or algo is implemented and exposed via a public method on an abstract base class, but one or more type-specific abstract methods on which the algo depends is implemented by subclasses in a type-specific way. This more "inverted" structure probably better highlights a distinction with a real difference. Your subclasses are inheriting major behaviors and "tweaking" the behavior as needed.
You could probably pull this off with interfaces, but it's more convoluted. You'd have one class with several external classes that implement some interface and have reduced access to properties of the original class. So, you may now have to expose properties that you'd otherwise prefer not to. And, you have to manage instantiation of the appropriate interface-implementing classes.
>it's entirely possible to "inherit" a base class without inheriting a single bit of information
Sure, you can override every method in a base class, essentially rendering it not inheritance. But, you could also not extend anything at all and opt not to use interfaces or any other OO-construct. It comes down to design.
> Java only distinguishes between interfaces and purely abstract base classes because one allows multiple inheritance and the other doesn't
No. This is what former C/C++ programmers coming to Java typically believe (some of them even after spending 10 years coding in Java, because old habits die hard).
interface = interface
abstract base class = implementation
Interface and implementation may seem like "a distinction without a difference", because one is just a list of method headers and the other is a list of method headers with method bodies, but semantically they are the opposite of each other.
Interfaces are a distinct concept from inheritance. Inheritance can be used as an interface mechanism, but that's merely an implementation detail. For example, Go has interfaces with no inheritance at all. It's unfortunate that most people think of Java or C# interfaces by default; it really creates a lot of confusion.
Yes, the timing belt or timing chain is inside the engine casing and it can be bad when it or subordinate parts fail. Though a timing belt/chain failure causing a catastrophic event for the engine itself, I don't think that's very common. What I'm referring to is called the serpentine belt and is often accompanied by what's called an accessory or AC belt.
This description and definition discussion describes very well the definition lawyering and specsheet-rewritting that agile avoids, by having the walking specsheet (customer) sitting in during the creation process.
My 91 pickup and 99 minivan's engines are still going strong with well over 200k miles, rarely having anything major wrong with them in the entire time I own them, whereas my 2004 and 2009 cars both with ~100k miles have been nothing but total maintenance nightmares. Timing issues, computer failures. Sheer nightmares.
> 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 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. What you can do is construct a new rectangle and destroy the old one in the process.
In other words, if you expect to change the shape of the item, you should not permanently identify the item by its shape. The given example is a bit like saying "The superclass Animal has a method becomeCat which turns any animal into a cat." and then feigning surprise when your code breaks for any non-feline animal.
The square vs rectangle example is a great one for showing how to not use types at all, IMO.
> As a type, this relationship is reversed: you can use a rectangle everywhere you can use a square (by having a rectangle with the same width and height), but you cannot use a square everywhere you can use a rectangle (for example, you can’t give it a different width and height).
Which is a good thing (yes, I'm a proponent of strong typing, like in Rust). If you want that square to be used in places where only a rectangle can be, you should either be explicitly required to cast it to a rectangle type or have a language that can define and handle contravariance for a consumer method, where it would be OK to accept a square as a rectangle.
EDIT: As this is getting upvoted and I said it slightly wrong: "where it would be OK to accept a rectangle where a square (or it's supertype, the rectangle) is acceptable." Goes ways to prove that strong typing is the better solution, though, I guess. :-)
> 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
I think you have let functional programming and immutable data-structures bias your world view.
The real world is mutable and the wonder of the digital mutable world is that it is pretty much just will-alone that can set attributes as you describe. It is immutability that is a trendy but artificial layer on top of this reality.
Only if you ignore time. It's not possible to change the state of the world at some previous instant (as far as our understanding of physics is concerned). The mutable world model is an artificial construction that aligns with our human perception.
Something that changes over time is mutable. Time exists and we can’t escape it, even with time-indexed immutable data structures that implement explicit mutability. The only question is does the thing change internally (an object) or is it replaced with a new one (a value).
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.
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.
> Your ice cube was never just an ice cube in the first place
That's my point. This idea that there are objects with mutable state is a myth. Even at the smallest scale, what we call elementary particles, are abstractions. There is nothing except the state of the universe at a given instant in time.
Agreed, but if we're accepting an abstraction instead of reality, than arguing for mutability because you think it's reality doesn't make much sense now does it? You're saying it's okay to abstract things away from molecules, but it's not okay to abstract things away from mutation. Why?
(Note that I'm not even persuaded yet that mutation is reality. I'm just saying that even if mutation were reality, it doesn't follow that mutation is the best abstraction with which to model reality.)
> Bob at time t is still Bob at t+1 even if his state (like position) has changed.
Again, I don't think this is at all evident.
What if we remove a limb? Give him a dose of LSD? Give him a brain tumor? Replace most of his cells as happens to each of us every few years? What change is large enough that it's easier to represent with:
bob = getNext(bob);
...rather than:
bob.foo = bar;
? Is it even harder to represent change one way rather than the other?
Indeed, socially, the concept of identity is a leaky enough abstraction to cause problems; for example if we are criticized for actions that we took years ago, it's easy to get defensive even if we are a different enough person now that we would never take that action now.
Bringing this back to inheritance: if it would make sense to model Bob as an instance of Child one day and as an instance of Octogenarian another day, why not create a new instance of Bob?
You want to argue this from the standpoint of "this is how things are" but I think that there are multiple ways to model Bob and which is appropriate actually depends more on the needs of the system than the true nature of Bob.
Bob’s state might change, but he is still Bob. That would also go for the Ship of Theseus. However you represent it, change is still change, it is still mutability.
We can argue if identity is useful in real life, but most people cling to names and identity, it isn't a controversial subject.
Dynamic inheritance is extremely useful in a programming language, though only a few have it. It is a very OO concept, as are the languages that have explored the concept (e.g. Self, Cecil, among others, even Javascript has this aspect, though not focused enough to use very well).
Ironically, the latter part of what you quoted shows the solution to representing change in an immutable system: create a different stream object instead of mutating the same stream object.
(Streams in this case not being streams in an I/O sense, just an example of a class).
Meh. Once you mutate an object it is not the same object it once was either; you've only maintained a stable memory reference and alias, without the overhead of making a new one.
No. Mutation essentially breaks subtyping. The infamous ArrayStoreException in Java is the story of how references (which should be invariant) are treated as covariant and therefore causes runtime exceptions.
If your array type (the only reified generic type in Java) is going to be parameterized by a single type, then you're right that it needs to be invariant.
One alternative would be to keep track of two types for an array: the most general type that can be stored into it and the most general type that might come back out of it. Any type cast that makes the types storable more strict or the type expected coming out less strict should be allowed. If you throw in a bottom type in your type system, then you get immutable arrays for free.
Of course, such a type system would probably cause a revolt among the majority of Java programmers for being too complex.
No. When references or arrays of references are treated as contravariant, you would have the opposite problem of ArrayReadException. You can now store things into arrays safely but you can no longer extract things from arrays safely.
This is not wrong, but it ignores that in terms of reliable software architectures, it’s very often still better to model change using strictly immutable data types. Often, not always.
As usual the truth is somewhere in the middle, which is why I don’t like the periodically recurring discussions why OO is bad, FP is good or vice versa. I think the article splits up the issue quite nicely, the conclusion I get from it is that the answer to the question whether to use inheritance or not is ‘it depends’.
The real world is mutable in some sense, but our thinking, even in the most trivial exercises, works by detaching _objects_ them from their physical nature and building immutable _ideas_. Thinking is a struggle trying to find what is necessary in a contingent world, trying to find essence in a world of form. That is why values and immutability are so useful for modeling, architecture, and building resilient software.
In real world you stretch a square and it may become a rectangle. So in OO even type of an object should be mutable if you want to model real world closely.
I don't know about this. Is a teleporter a vehicle? What about a vehicle which doesnt have wheels, in a world built around assuming that vehicles have wheels?
>I don't know about this. Is a teleporter a vehicle?
Depends on your definition on vehicle. For many purposes (e.g. going somewhere) it could be, especially if it also existed.
Subtyping just means "having a taxonomy of things, where we can substitute things lower on the taxonomy -- more concrete, more specialized and so on" with things higher up when wanting less abstraction (and vise versa).
And this is pretty much the case with the world -- Plato's "ideas", scientific taxonomies (e.g. of animals and plants), or RDF and semantic taxonomies, and so on are all based on the same concept.
(That doesn't mean they're perfect descriptions of the world, like our class taxonomies in OOP don't lead to perfect descriptions of most problem domains either).
>What about a vehicle which doesn't have wheels, in a world built around assuming that vehicles have wheels?
That's a problem of definition of what should belong in the class of things we call "vehicle" (and you could have the exact same questions when doing OOP).
As such it's not an argument in whether the real world has or doesn't have subtyping.
Values should be immutable even in OO languages. A point’s x and y coordinates can’t be mutated because then it would be a different, just like a number’s value can’t be mutated. The question is then is a rectangle a value or an object? I would say no and then stack allocate it like any other value, recomputing it if a rect property ever changed.
The real world has both values and objects, any ideology that doesn’t acknowledge both is just fooling itself.
> 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.
I think traits (as I know them) are still nominal subtyping. E.g.: a Rust struct implementing a next() method does not implement the Iterator trait even if they are structurally equivalent. You have to explicitly implement the trait.
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.
Which is another way of saying that the domain model is what determines the properties of polygons that actually matter. You may have two classes, Square and Rectangle, and they may be mutually incompatible because you need to represent the shape of a hole and the shape of a plug. In that case your square isn't compatible with a rectangular shape even though it's a type of rectangle.
I think the author is saying that sometimes people are unclear about the domain model, and OO doesn't really give you any tools to reason about the model that you don't have in imperative languages, so it's not really an improvement. Fancy type systems are not a substitute for understanding the thing you're reasoning about.
> Fancy type systems are not a substitute for understanding the thing you're reasoning about.
And now you got me wondering about all the times I wrote a Haskell type, some bugged code, mindlessly changed code until the compiler stopped complaining, and it worked flawlessly.
But I guess your comment was about that "writing the types" stage. There were a few times I mindlessly wrote them too based on where they would be used (I could have the compiler give me them, but I'm not used to it). I can remember a few times I did both on the same functions, mindlessly write the types and the implementation.
> 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.
But a rectangle as traditionally defined can have equal dimensions. Consider the rectangle whose width slowly grows to be longer than the height. Surely it does not skip a value because it happens to equal the height?
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 rectangle whose shape is temporarily square until a setter is called. The "square" invariant doesn't hold for the object's entire lifetime, so it's not a square. Instead you could write an isSquare method and sometimes it would return true, depending on the object's state.
The inheritance relationships you expect from thinking about math only hold for immutable objects.
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.
Yes, and that's directly related to immutability. Immutable objects are timeless; their initial state is their final state. Any property that holds for their initial state can be encoded in the type and placed somewhere above it in the inheritance hierarchy.
You can think of a mutable object as a collection of immutable and mutable properties, where only the former can be moved into the type and become part of the type hierarchy.
This distinction isn't really about inheritance; it's more about what a type is in the presence of mutability. The type represents the invariants.
I think I get what you're saying here. On some level, an object method is a function which takes an object of a particular type and returns a new object of the same type.
makes no sense, and it's pretty clear why. The return value is wrong. You can use a subclass as an argument to a function, but you can't return a superclass and just assign it to a subclass. And if you have some function
void Operate(Rectangle& r) {
r = GetRectangleWithWidth(r, 2.0);
}
then passing in a square should be a syntax error and not compile. Although, I'm not sure what C++ would actually do in this case.
>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 who makes an X by Y board), or at anytime later (e.g. the same table maker who cuts the a 3X by X board into a 2X by X board).
Even more so with other materials, where we can just bend them and squash them into the dimensions we want again and again (e.g. pastry dough).
I disagree. If you remold a square piece of pastry dough into a circle you haven't "circled the square", you have destroyed the square entirely!
I don't quite get the army metaphor, but I don't think the relationship of "citizen" and "soldier" is analogous to that of "rectangle" and "square" or even "rectangle" and "rectangle with different dimensions".
>I disagree. If you remold a square piece of pastry dough into a circle you haven't "circled the square", you have destroyed the square entirely!
Well, the physical thing is all still there, just in a different shape.
You have destroyed only an abstract quality, it's squareness, not the thing.
So you haven't "destroyed it" entirely anymore than changing the width of an object to something equal or different than it's height its 'destroying" the object. It just sets a value for a field of the same struct in memory.
Whereas your argument was "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".
If by the rectangle you mean the abstract quality, then you're right, you can't.
But we're talking about an object in computer memory that's an instance of a square or a rectangle, or about some physical thing that is one or the other shape.
And if you mean those, then yes, you can very much will that object to a different width and height.
Which is the one of the distinctions TFA makes: the abstract quality can't change, but the thing that embodies that quality can change qualities (and, with them, attributes, like width and height).
> You have destroyed only an abstract quality, it's squareness, not the thing.
Here we are in agreement.
> But we're talking about an object in computer memory that's an instance of a square or a rectangle, or about some physical thing that is one or the other shape.
Everything in a computer program is an abstraction, and of course you can arrange these abstractions however you like. You could create an instance of a Square that inherits from Paper and has dimensions of "A8". I'm merely arguing that when building an object whose shape may change, using a PastryDough class is probably preferable to using one called Square.
Agreed this example is deeply flawed! As you point out, there is a difference between what you can do with an object versus how you can create one. And "you could use SOME rectangles anywhere you could use a square" is not even a claim about type relationships... It would have to be "you can use ALL rectangles everywhere you could use ANY square" to reverse the type relationship.
In reality, abstract and usage typing are highly aligned. The incompatible sibling is code inheritance. Knuth put I more simply: "Don't inherit to reuse code, inherit to be reused."
To fix it in real world you make factory abstraction which you feed with initial data and then factory knows if it is rectangle to be produced or a square. So developer should not be concerned with cats or dogs, developer should be able to call Feed() method.
I took over the maintenance and enhancement of a customer's eCommerce site.
It's written in Python, using an ancient framework - Pylons.
The person who originally designed and wrote it, coded large super classes and then subclassed off those.
And then somtimes subclassed off those subclasses.
So now I'm lost in a maze of twisty classes/subclasses, which makes maintaining and enhancing this eCommerce site much, much more of a challenge than it should have been.
In a few particular cases, I've found myself having to move functions out from one subclass and into the superclass, in order for related subclasses which needed access to it, for me to be able to add more functionality to the site.
I think what I'm trying to say is - from my experience, you can use Inheritance in many, many ways which can make life very difficult - nay, miserable - for your future self and especially for anyone 'inheriting' your project.
In my own home-grown Python projects, I have actually yet to use Inheritance (in classes I write myself - there's no escaping it when you're using other libraries of course), even after years of Python coding, because when I'm coding a new class DoSomething(), it's for that particular task and to date I've not ever had to subclass any of my classes.
> you can use Inheritance in many, many ways which can make life very difficult - nay, miserable - for your future self and especially for anyone 'inheriting' your project.
The principle of “Composition Over Inheritance” is really helpful. You should google it if you’re not familiar! It’s often a better alternative than deep inheritance hierarchies.
I agree. I never find myself with a scenario in which inheritance is useful. I actually started writing a lot of Go (which has no inheritance support) and haven't missed inheritance at all. It really seems like useless complexity.
I've found one or two cases where "inheritance would be nice". It wasn't necessary.
I've found so many cases like the OPs where it made things harder, and the fix was basically pulling apart all shared functionality, where the only thing inherited were interfaces (if even that).
Inheritance was probably was a good idea for a small thing and quickly achieve certain goals until features ballooned and he keep promising to deliver more features on top promptly
There’s a rule from XP that people still ignore at their own peril. The rule of Three is, for those who tend to overengineer, a plea to wait a little longer. But for everyone else it’s a call to action.
When you hit three copies of a pattern is when you should reconsider your choices. Just because a pattern was fine ten minutes ago doesn’t mean you should keep doing it now. At some point it has become “too much and as part of the campsite rule you have to ask if the block of code is ridiculous. It may have already been ridiculous, or you might be the one who took it there, but that doesn’t matter because you’re here now and what are you gonna do about it?
OO and inheritance can be brittle when you get new features that affect a wider amount of existing code. If you're only adding stuff on incrementally, you can conceivably add new classes for all of it and not worry about working with the existing code (usually you likely have some business classes that are basically starting primitives for new features, e.g. Customer).
As it gets more brittle, it gets harder to add those spanning features, especially if it's not clear what features will be asked for.
> In a few particular cases, I've found myself having
> to move functions out from one subclass and into the
> superclass, in order for related subclasses which
> needed access to it, for me to be able to add more
> functionality to the site.
That seems like a refactoring basic. Far from being miserable, it is likely the easiest way to have met the requirement to add functionality.
The misery comes from zero documentation, no code commenting, very short variable names, and having to trace backwards from the subclass to the superclass to the ancient frameworks and libraries being used.
Especially tracing backwards from a subclass and it's undocumented! :)
And you are probably in the same situation when he was in when he realized it’s too much to re write and so he continued down the path. The next person will have the same issues as you until one of you can get Managment to let you rewrite it.
Oh, come on. The title submitted to HN is "Inheritance often doesn't make sense"; the actual title of the article is "Why inheritance never made any sense". Are you kidding me?
Do we really need, in 2018, another article continuing this particular religious war? Inheritance is just another tool in the software engineer's toolkit. When you need that tool, use it; when you don't, don't. But taking a position where you say it's never the right tool or, conversely, always the right tool makes you sound ignorant and inexperienced.
I didn't get that from the article at all. I actually like what the article is doing a lot, and I wish people would take the same approach more often. When arguments about high-level concepts like inheritance go poorly, it's usually because everyone involved is talking about something slightly different. Maybe a supporter of inheritance likes the way it lets them think about their program (ontological inheritance) while a detractor doesn't like how it forces each subclass to carry along the baggage of its superclass (implementation inheritance). These people are not going to have a fruitful discussion without understanding that they're talking about different things.
The concept of "types" in programming languages is another great example -- there are syntactic type declarations, memory-level types to specify which bytes mean what, things like typeclasses or interfaces which give you runtime polymorphism, the "type" you have in your head when you're thinking about what kind of data your code needs to handle... before you even start to have a discussion, you need some idea of what you're talking about.
The problem balancing implementation vs ontological inheritance while providing strong type safety is that your language needs to either support defining contravariant types (and you still likely end in a mess, see Scala's eternal discussion about "total" type safety), or you disallow ontological inheritance completely (like Golang, and therefore cannot support many modern programming features, for the better or worse, doesn't matter). Not sure if there is a language that truly does the opposite by design, though (being strongly typed and disallowing implementation inheritance, while providing ontological inheritance). Such a language might be the DDD modeller's heaven? :-)
> or you disallow ontological inheritance completely (like Golang, and therefore cannot support many modern programming features
Sincerely, what features does this preclude. I pretty much ignore ontological inheritance in any programming language because it never seems useful. Am I missing something?
> But taking a position where you say it's never the right tool or, conversely, always the right tool makes you sound ignorant and inexperienced.
Neither of those positions is advocated by the article, so who exactly are you talking about?
In fact, you seem to have assumed that "make sense" in the title referred to the issue of whether or not to use inheritance, whereas in fact the article is concerned with the customary conceptual understanding of inheritance within software engineering, and whether this understanding "makes sense".
This is an easy enough mistake to make simply from reading the title, but one that should have been cleared up in a matter of seconds once you started reading the article body, which I presume you did, rather than immediately rushing to the comment section?
It's hard to tell whether you didn't read the article, or you read it but were so preoccupied with an existing train of thought that you essentially responded to an imagined version of it. I would generally agree with your point if it was about one of the many articles that do try and make this point but I don't see where this one does.
Agree. Tools are only as good and effective as those who use them. At some point we have to be responsible for the decisions we make. And sometimes biz needs change to the point that those decision change from right to wrong. Just me?
I hear the author. Just the same, at 50k ft, it just seems like the same subject on a different topic. That subject is: Peogramming / software engineering is imperfect. And sometimes things break and need to be refactored.
Why should this profession be any different than any other. The quest for The Holy Grail of Perfection is nobel, but we'll never find it.
I'm not suggesting sloppiness and lower standards, but a sense of theory is great but the reality is reality will never be that perfect.
Use the right tool available at the time. When that tool is no longer right and/or effective then replace it. Keep moving.
We haven't been given an expiration date regarding ignorance and inexperience so it's no surprise we see it in 2018 or that we will continue to see it well into the future. An interesting question is whether the amount remains relatively the same or there's some "evolutional" trend, but this seems very hard to both quantify and qualify.
>But taking a position where you say it's never the right tool or, conversely, always the right tool makes you sound ignorant and inexperienced.
Even more so if you're arguing against a strawman position never even mentioned in the article...
"Why inheritance never made any sense" in the title is meant as: "here's why people are often confused by inheritance, and here's a better way to think about it".
> A common counterexample to OO inheritance is the relationship between a square and a rectangle. Geometrically, a square is a specialisation of a rectangle: every square is a rectangle, not every rectangle is a square.
Alternatively, you can argue that a rectangle is just a square with the additional freedom to vary the width and height independently. So in C++ syntax you would have:
class Square {
protected:
size_t width;
// No need to have "height" field because Square guarantees that height ==
// width.
public:
virtual size_t getWidth() const { return width; }
virtual size_t getHeight() const { return width; }
};
class Rectangle : public Square {
size_t height;
// A rectangle allows height to be different from width.
public:
virtual size_t getHeight() const override { return height; }
};
See? This makes OO hard because it's hard to decide which should be the subtype of which intuitively, and people get even more confused since the arrow is contravariant in its left argument and the relationship would sometimes appear reversed. There in fact is one correct answer but people often get it wrong. You need some serious appreciation of OO to know which.
Here are some more examples that can cause confusion. Suppose we have Reference (which can be read or written), ReadableReference (read-only) and WritableReference (write-only). From a purity perspective, should we have Reference inherit from both ReadableReference and WritableReference (ignoring OO languages that don't allow multiple inheritance), or should we have ReadableReference and WritableReference inherit from a single base class Reference? This kind of question frequently trips people up.
You reduced the "Square" class to a convenient implementation helper.
Whatever type they choose to use, clients of your class hierarchy now only see one interface: getWidth/getHeight.
And they can't make assumptions based on the type they see, as both "Square" and "Rectangle" might have "getWidth()!=getHeight()".
So they must handle the generic "rectangular" case.
So, for the client, there's absolutely no visible difference between a "Square" or a "Rectangle". Which means there's no point in dealing with two classes anymore : all client code will then take "Square" objects, and deal with them as geometrical rectangles.
> There in fact is one correct answer but people often get it wrong. You need some serious appreciation of OO to know which.
Please enlighten me which one is correct, and I’ll happily argue that the other is in fact correct!
(I think you’re going to say Square extending Rectangle is correct lest Rectangle break Square’s invariants, but I’m not certain. What language you’re operating in may influence the matter; for there can be very important differences in how different languages handle variance which can invert the answer. I’m a little rusty on this, though, because I haven’t had to actually worry about it for a few years since I last wrote Rust code where the variance of a type with respect to a generic parameter actually mattered, and for work I’m mostly writing JavaScript where it’s all fuzzy enough that you pretty much get to decide what is right and what is wrong!)
If you want to argue about OO you should use Smalltalk. Many other languages use some shortcuts (often for performance reasons) which ignore central concepts of OO (e.g. 'everything is an object').
I find that inheritance as a code reuse tool pushes developers in the direction of taking whatever class they have and tuck some new properties to a subclass.
On the other hand, if you realise that a square is a rectangle with the same width and height, why should you bother at all with creating a new class? A new constructor (or an helper function) should be all you need
> On the other hand, if you realise that a square is a rectangle with the same width and height, why should you bother at all with creating a new class? A new constructor (or an helper function) should be all you need
What if you have a function that only knows how to operate on squares? Forcing it to accept Rectangles will be unnatural. Of course we can use just a single type Rectangle with two constructors, but inherently that's throwing information away about certain conditions we know to be true.
Even in functional programs when we don't have inheritance, we still sometimes need conversion functions between related types and whether or not these use option types illustrates essentially the same idea:
data Rectangle = Rectangle Int Int
data Square = Square Int
recToSq :: Rectangle -> Maybe Square -- may not succeed; like downcasting
sqToRec :: Square -> Rectangle -- always succeeds; like upcasting
Indeed, this is exactly what the "coercion semantics" in subtyping means: whenever S is a subtype of T, we can generate a total function from S to T. I believe this is also called "inheritance as implicit coercion" in https://www.sciencedirect.com/science/article/pii/0890540191... Quoting from the abstract:
> We present a method for providing semantic interpretations for languages with a type system featuring inheritance polymorphism. […] Our goal is to interpret inheritances in Fun via coercion functions which are definable in the target of the translation.
> What if you have a function that only knows how to operate on squares? Forcing it to accept Rectangles will be unnatural
then you write another function that does what you want with rectangles. I think that most of the problems with inheritance stem from unnatural obsession for code reuse such as this.
This is a valid point, but now you have two ways of representing a square and you need the coversion functions.
It's better than nothing as you can guarantee that an instance of Square is a square, but you still need to handle squares disguised as Rectangle.
You could throw in dependent types and force rectangles to have different width and height, but that doesn't really help in getting nicer API.
If I had to do something like this in a real program I would take these issues as a sign that I need to step back and re-evaluate my design. I'd look at what I want to use these types for and try to come up with the best approach for that specific case.
Thanks for the example. Yeah this is another case where people have trouble understanding subtyping relation and which type should be the subtype of which.
And I agree with you that the opposite should be true conceptually, because if you ask yourself, "if a partial function is being demanded, can you use a function instead?" you might answer yes.
Alternatively, think of a partial function conceptually as a function that returns an option type, i.e. in Haskell syntax
type Function a b = a -> b
type PartialFunction a b = a -> Maybe b
and with the obvious addition that "b" is a subtype of "Maybe b", then we again conclude that function should be a subtype of partial function.
Great post. I would go as far as claiming "don't really mess with inheritance until you have not internalized the principles of co- and contravariance". Might be a bit extreme, but I see no way how you can safely navigate OOP without (see Java's famous problems with getting this wrong on container types...).
The deeper problem is that many domains cannot be modelled as hierarchical taxonomies, but only as ambiguous categories. This is a theory expounded by, amongst others, George Lakoff in his book "Women, Fire and Dangerous Things"[1] and elsewhere. Such cognitive categories are fuzzily defined, often overlap, and represent a collection of traits and properties that members of a category share some or all of. But different members can belong to the category to a greater or less extent, or be better or worse examples of it. E.g. both ostriches and sparrows are birds, but sparrows are usually considered a better example of the category birds.
Like the process of developing scientific taxonomies, efforts to model these kind of domains using OO techniques frequently run into edge cases that defy concrete classification. E.g. can all birds fly? Clearly, flight is an extremely important trait of the class "bird", but we also recognise flightless birds as birds, which means objects can lack an important trait of a category, but still belong to it, albeit as a worse example.
This is not just a theoretical problem. Imagine trying to invent a set of classes to model computing devices for an IT asset management system. What exactly constitutes a smartphone, vs. a tablet, vs. a laptop, etc.? It's easy to imagine the attributes and behaviors you might attach to these classes, but properly mapping every real world device onto them is extremely difficult. You are always going to get edge-case devices that straddle multiple classifications, or exclude some behavior that is assumed to be critical to devices of its type.
The result is that systems trying to model such domains end up with a variety of problems: Some impose a rigid, but essentially arbitrary, taxonomy, with the result that users struggle to use it properly. Edge-cases require special, out-of-band handling, e.g. users understanding that the system cannot properly represent items of a particular type, and that you have to accept a misclassification, or misuse the system slightly in order to make it work.
Some systems flatten and generalise the taxonomy into a single superclass, perhaps called "Device" in our example, that holds all the attributes and behaviors of all the former subclasses. This requires users to then interrogate objects of this generic type and essentially determine their "actual" type on an ad-hoc basis, with the result that they may be treated differently, or incorrectly, by different processes, depending on the heuristics that were applied in that case.
And finally many systems just abandon the idea of internally representing the taxonomy in concrete terms at all. As edge-cases proliferate, and that idea that "every X is just a specialised case of Y", is taken to its conclusion, the concrete taxonomy dissolves and is replaced with a general taxonomical abstraction. A new, more flexible object system is implemented on top of the native object system, such that users of the system can define their own classes, at runtime. These systems will support tagging and multiple membership, and other devices for representing complex taxonomies. This solves the problem of representation, but only by abrogating responsibility for defining it entirely to users of the system. These users are then faced with the task of developing the taxonomy from scratch in userland. In an organisation where different departments need to agree on a common taxonomy, this process is often centralised, and a single simplified set of classes is presented to sub-users. But then edge-cases to this taxonomy start to emerge, and the process repeats itself. This is how layers of abstraction proliferate.
Well, the conceptual view is always first. So in fact a square is a subclass of a rectangle. Implementation comes second, and has to find out how much code sharing is possible.
I prefer the data representation comes first, especially in a language like C++.
Maybe you can represent 1000 squares with 1000 integers. If you need polymorphism on top of that, there are plenty of approaches without rearranging or copying the data to make it fit a type taxonomy.
This article seems to be confused about what abstract data type inheritance is, as exemplified by "As a type, this relationship is reversed: you can use a rectangle everywhere you can use a square (by having a rectangle with the same width and height), but you cannot use a square everywhere you can use a rectangle (for example, you can’t give it a different width and height)."
If an abstract data type X inherits from Y, that means that every X can be used where you can use a Y. You can only use a rectangle where a square is expected if that rectangle happens to be a square. Inversely, you can use a square everywhere you can use a rectangle: if you change the width and height of a rectangle, you turn it into a rectangle with different width and heigth; if you change the width and height of a square, you also turn it into a rectangle with different width and height.
That also means that you can't have mutable values, static types and subtyping in the same language: if you apply a mutating function defined for a supertype on a subtype, you might end up also mutating the type of the mutated value by invalidating invariants of the subtype. It's the same problem that made covariant arrays in Java unsound.
If the type system is sound and expressive enough, ontological inheritance ( this thing is a specific variety of that thing) and abstract data type inheritance (this thing behaves in all the ways that thing does and has this behaviour) should be essentially the same thing.
> That also means that you can't have mutable values, static types and subtyping in the same language
Yes you can. You just need to make a clean separation between mutable variables and immutable variables. Then mutable variables must be invariant, and immutable variables can enjoy subtyping.
Alternatively, classify mutable variables further and make references carry information about whether only reading/writing is allowed through this reference. Then read-references enjoy the usual covariant subtyping, and write-references enjoy contravariant subtyping.
Agreed, although I'd go a step further. A mutable variable has a type of reads and a type of writes. They vary in opposite directions. If you constrain them to be the same then they must therefore not vary at all.
Unless your program's goal is to ontologically categorize objects, #1 is a trap that will just bite you in the ass.
In the physical world, if I have a rectangle shaped box I want to cover, I don't want a square, no matter how much "squares are rectangles". It's no different in a program. What really matters is the behavior and goals of your program. It's cute to say squares are rectangles but if your program needs to let your user independently set width and height then a square is completely useless.
> Unless your program's goal is to ontologically categorize objects, #1 is a trap that will just bite you in the ass.
Strangely, ontological inheritance isn't even useful for that purpose. If you're organizing things ontologically, you certainly want to have runtime access to the relationships in your ontology, which means you should model things as data, not static class relationships (although reflection can turn static class relationships into data, it's a pretty hokey, indirect solution).
Most languages that have classes expose inheritance relationships in someway without reflection. Even JavaScript provides a non-reflective instanceof now. You can also reify inheritance relationships more directly by hand of needed.
I feel as though the rectangle/square example falls down mostly because of a linguistic trick.
Rect->Square could be a perfectly reasonable type hierarchy, as long as you don't allow self-mutation of the very attribute that defines this specialization. But then, mutation tends to wreak havoc on inheritance anyway (e.g., covariance/contravariance) -- and almost everything else. When I take the needle out of my record player, it can't play records any more, so is it really still a "record player"?
This isn't special to any particular type of inheritance, either. If you let all the air out of your ball, it's no longer a sphere, so even with purely ontological types, you're already in trouble. Inheritance isn't the problem. Mutation is.
This would be overengineering, but the following six types would solve the problem:
* ImmutableSquare
* ImmutableRectangle
* MutableSquare
* MutableRectangle
* Square
* Rectangle
The "Mutable..." classes are mutable, the "Immutable..." classes are immutable, and the "Square" and "Rectangle" classes mean that you can read the values, but there is no guarantee about either mutability or immutability.
In this system, "ImmutableSquare" and "MutableSquare" are subtypes of "Square"; "ImmutableRectangle" and "MutableRectangle" are subtypes of "Rectangle"; and also "ImmutableSquare" is a subtype of "ImmutableRectangle" (and therefore "Rectangle").
But if you go this way, don't be surprised when you end up with millions of classes.
In some languages, there are also (non/)threadsafe variants of mutable classes. As you say, this approach blows up quickly.
If I saw that, I would ask what 'problem' it's trying to solve. Is mutation actually the goal, or simply the means? In languages which don't support general mutation (I'm writing Clojure right now), I really don't miss it at all.
The reason implementation inheritance with substitution (aka "virtual methods") is bad is that it results in classes having two APIs: the normal public API and one for inheritors, the latter of which is usually undocumented, and even worse the APIs are conflated.
To see the issue, imagine you have a class representing a thermometer, with two virtual methods: get_c() and get_f() returning the temperature in Celsius and Fahrenheit degrees respectively.
Now it turns out that a specific model of that thermometer was miscalibrated and always returns 10 C more, so you decide to make subclass that corrects the behavior.
Unfortunately, that's impossible (without composition or mutable state).
A first attempt could be to override get_c() to call the parent and subtract 10. However, get_f() will still be wrong unless it happened to be implemented by calling get_c() and converting.
A second attempt could be to override both and apply the correction to both. Except now if get_f() is implemented by calling get_c() and converting, the correction will happen twice!
The issue here is that how the class uses its internal API is undocumented, and also that the internal and public APIs are conflated, making it impossible to change only one.
This can be solved by never having a class call overridden functions on itself, but that just results in a system that is equivalent to composition with delegation.
If such overriding is really required, it can be accomplished by adding a "callback interface" parameter to the constructor and documenting how it is called and how it's expected to behave.
I don't understand why some functional programmers have so much difficulty with the concept of inheritance. Discriminated Unions and pattern matching is just inheritance and virtual dispatch turned inside out. Yet no one is naval gazing about whether their algebraic datatypes are 'ontological' or not.
I also really wish people wouldn't try and dismiss concepts they're ignorant of:
because multiple inheritance is incompatible with the goal of implementation inheritance due to the diamond problem
I have no idea what that sentence is supposed to mean. It doesn't make sense even given their own definition of implementation inheritance.
Actually, I think they are kind of duals to each other. With pattern matching it is easy to write new functions but hard to add a new case (you need to edit every previous pattern match you wrote). On the other band with classes it is easy to add a new class but hard to add a new method (you need to edit every single class you previously wrote that implements that interface)
Functional programmers call this the "expression problem"
Yes it's true that discriminated unions and pattern matching is just inheritance and virtual dispatch turned inside out. But discriminated unions and pattern matching are easier to use, easier to understand than inheritance and virtual dispatch.
And you don't need to struggle with principles like "is-a relationship" which can be confusing.
But discriminated unions and pattern matching are easier to use, easier to understand than inheritance and virtual dispatch.
That seems incredibly subjective. It's 50/50 for me, I just go with the grain with the language I'm using. I'll admit that in practice pattern matching wins due to the depressing lack of multiple dispatch in OO and pseudo-OO languages.
And you don't need to struggle with principles like "is-a relationship" which can be confusing.
Why would you struggle with that question with inheritance and not DUs? what is it about DUs that make it not an issue?
We don’t use inheritance anymore. If something needs to be shared it gets its own service class.
Every time we’ve used inheritance it’s ended up being more of a hassle to keep track of as functionality changed. I know we could utilize it better than we have, but that’s part of development management as I see it, if I know my crew won’t utilize something to good effect, then it’s often better to adapt our practices rather than fail at change management after a long period of trying. Especially because a lot of new hires are really bad at OO principles beyond the fundamentals.
Worst thing I've ever seen was a class which inherited another just to keep the lines of code short (<2500). That said it wasn't just one time inheritance... It was up to four times. There was no logical split of the inheritance. It looked like someone simply split one huge file into some smaller ones.
We called that kind of inheritance "code sharding" and we had a lot of headache at that time.
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 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. - Joe Armstrong
The artiste's perspective:
Everything tends to make one think that there is little relation between an object and that which represents it. - René Magritte, surrealist
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
The pedagogical perspective:
CSCI 2100: Unlearning Object-Oriented Programming - Discover how to create and use variables that aren't inside of an object hierarchy. Learn about 'functions,' which are like methods but more generally useful. Prerequisite: Any course that used the term 'abstract base class.' - James Hague
I think the problem is more that OOP and FP teach that the solution is in the language constructs, when in fact the solution is in the developer.
OOP and FP are typically taught as outputs. You take a problem, you apply FP or OOP magic, and you get a solution squeezed into an FP or OOP shape. The implication is that the language somehow half-solves your problem just by being how it is, and all you have to do is apply it.
IMO this is the wrong way to do things. Developers should be taught abstract domain modelling skills in a language-independent way, then taught how solutions can be implemented in different paradigms. Then they can start coding.
The solution is in the design of the relationships, not in the language syntax. And there's no such thing as a off-the-shelf one-size-fits all set of relationships.
There are domains where it makes total sense to subclass polygon through rectangle to square, and domains where that's a very bad idea and will cause endless pain. So it's not enough to say "This thing is like this other thing, so they both belong on the same inheritance tree." Sometimes the resemblance is superficial and irrelevant in terms of the domain - even though to you as a human programmer the similarities are "obvious."
Neither OOP nor FP can help you if your ability to design minimal but powerful abstractions that fit specific problems is poor. OOP and FP will expose you to new ways of thinking about problems, but neither is general enough to "just work" or keep you out of trouble if you don't truly understand what you're trying to do.
I think the problem is more that Roman and Indo-Arabic numerals teach that the solution is in the notation, when in fact the solution is in the mathematician. . .
I’m confused about your analogy. Indo-Arabic number representation enables one to mentally carry out computations that would be much more complex in Roman notation. The notation might not be the solution, but the notation certainly enables solutions not really conceivable in other notations.
I believe functions + immutable data enable me to mentally figure out solutions to complex problems that would be much more complex if solved with objects and inheritence and mutable state.
The analogy breaks down (at least for me) as there are some trade-offs being glossed over that don’t really exist with the different systems of numerals. What’s being lost for the conceptual simplicity you’ve gained?
Perhaps that works in theory, but in practice abstract domain modelling only gets you so far. In the real world to build a working system that solves a real problem you typically have to work iteratively from both ends simultaneously until you eventually meet in the middle. Do some modelling, then write some code to validate your understanding and understand what's really going to work, then repeat ad nauseum.
The classes of bugs you get in an OOP heavy conceptual modelling inheritance tend to be far more difficult to reason about, because half of the damn classes using it end up being utility classes, so you end up modeling a computer that does a thing instead of the thing.
> I think the problem is more that OOP and FP teach that the solution is in the language constructs, when in fact the solution is in the developer.
I would say the solution is in the appropriate construct. Do whatever means writing the least amount of code possible. Less code means less to maintain and less to execute.
The No True Scotsman fallacy leads to arguments like this:
Person A: “No Scotsman ever steals.”
Person B: “I know of a Scotsman who stole.”
Person A: “No True Scotsman would ever steal.”
Person A is thus protected from the harmful effects of new information. New information is dangerous, as it might cause someone to change their mind. New information can be rendered safe simply by declaring it to be invalid. Person A believes all Scotsman are brave and honorable, and you can not convince them otherwise, for any counter-example you bring up is of some degraded Untrue Scotsman, which has no bearing on whatever they think of True Scotsman. And this is my experience whenever I argue against Object Oriented Programming (OOP): no matter what evidence I bring up for consideration, it is dismissed as irrelevant. If I complain that Java is verbose, I’m told that True OOP Programmers let the IDE take care of some of the boilerplate, or perhaps I am told that Scala is better. If I complain that Scala involves too much ceremony, I’m told that Ruby lacks ceremony. If I complain about the dangers of monkey-patching in Ruby, I’m told that True OOP Programmers know how to use the meta-programming to their advantage, and if I can’t do it then I am simply incompetent. I should use a language that is more pure, or a language that is more practical, I should use a language that has compile-time static data-type checking, or I should use a language that gives me the freedom of dynamic typing. If I complain about bugginess, I’m told that those specific bugs have been fixed in the new version, why haven’t I upgraded, or I’m told there is a common workaround, and I’m an idiot if I didn’t know about it. If I complain that the most popular framework is bloated, I’m told that no one uses that framework any more. No True OOP Programmer ever does whatever it is that I’m complaining about.
I think OO has so many fierce defenders, due to the simplicity of the concept, allowing even the mediocre to paint a system by the photograph of reality as it is imagined to be.
In this Object Orientated Fairy world even the Programmer has a place- he usually becomes the "Controller" Object - passing messages and half finnished objects between components.
Everyone can do this, everyone can relatively fast read into this. And this is why object orientation persists. It has no value beyond that.
Inheritance is one of those concepts, people went overboard with in the 90s, generating matroschka objects- that only the compiler could read and whos methods without a IDE where unguessable.
Since then- we put it into the history box, and replaced it with composition, which is inheritance in all but syntax and creation call hierarchy.
The attack on the holy mountain of OO is thus doomed to be unsuccesfull, because those who are besieged up there can not come down here- because they allready struggle with "normal" code and are happy to code down reality as seen by some "architect".
Efficiency or adequate abstraction, is not actually part of the discussion. Neither is speed.
What it provides is accessability- and that it provides rather well. Its a crutch for human minds- and a good one at that.
I don’t think those are No True Scotsman fallacies. Or at least not as you describe them. The problem appears to stem from using specific languages to argue about a broader concept. Java being verbose, Scala having too much ceremony and Ruby allowing monkey patching aren’t really complaints about OOP per se. Nor are issues of static or dynamic typing, bugs or bloat.
For example the discussion here hasn’t seen a lot of shying away from the practical and conceptual issues with inheritance.
The No True Scotsman fallacy is based on the arguments of a single interlocutor who is trying to evade the conclusion.
Your description sounds more like a series of different conversations with different people at different times starting from different topics. Since people, times and topics vary, yeah, you'll get different counterarguments.
Especially since, like all programming theories, the content, meaning and boundaries of OOP are widely disagreed on.
What are you trying to argue? Like what's the actual proposition that you're putting forward?
Those mostly sound like scattered, particular complaints about particular OO languages, none of which have anything to do with OOP as a paradigm (as far as I can tell).
If you're trying to suggest that there is no viable OOP programming language, that's manifestly false, as trillions of dollars of value have been created following the OOP paradigm in various OOP-oriented languages.
If you're trying to argue that there is no perfect OO language, I'd agree with you but argue that the it's a trivial consequence of there being no perfect programming language in general (language design is a game of trade-offs).
If you're trying to suggest that every use of OOP could be replaced by a clearly superior, readily available, battle-tested non-OOP approach (which you haven't, you've just listed languages and language flaws with no alternatives), the argument sounds pretty implausible.
You can weaken some of these claims to make some headway (replace always with in general or whatever), but that kind of takes the wind of out the argument's proverbial sails.
Personally I dislike OO approaches because I usually work on greenfield projects, and every example of OO in a greenfield context that I've seen has been deeply premature and slowed down development. (I also think the DRY principle is a pile of crap in a greenfield context with highish-level code). When I pull down a gigantic, mature Apache repo though, I'm usually not put off by mature OO abstractions, and they (in general) help me follow and add to the code. It cuts both ways. Like most ideas cooked up by smart people solving hard problems, OO is imperfect but sometimes useful.
> The notion of object oriented programming is completely misunderstood. It's not about objects and classes, it's all about messages. - Alan Kay
In other words it is about avoiding brittle hard-coded dependencies. When you "send a message" you can't depend on the implementation of the responding party. I think that's the general idea.
> 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.
That would be next to impossible, since the term is overwhelmingly used by working programmers in the context of C++/Java/C#/etc. Also, I suspect that the only reason Kay's version is even brought up in every discussion of the term is some sort of programmer-hipster signaling thing. Not that Kay's OO isn't important - it is. It's just hardly ever relevant to the actual discussion.
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 are “signaling”, you’re assuming the worst about people before seeking to understand them.
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. Which opinion should I have engaged with?
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!
> ... 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 a very interesting property, and you'll also see it in statically-typed languages which support row polymorphism, like OCaml. OCaml directly supports OOP and subclassing, but it also supports structural typing, which means that objects are type-compatible if they support the same methods.
Unlike Smalltalk, this is all resolved at compile time, so it's quite efficient.
Introducing template metaprogramming comes with its own passel of problems: compile times (and code size) can explode, and then there are the difficult-to-debug error messages if you screw up. Because Smalltalk uses latent types, these decisions are deferred till run time, which is a loss for static analysis, but doesn't require a separate generated bit of code for every combination of arbitrary types used in the program, and it can be debugged on the spot without recompiling much more than a single method.
> Introducing template metaprogramming comes with its own passel of problems: compile times (and code size) can explode, and then there are the difficult-to-debug error messages if you screw up
I don't know any even remotely large recent C++ project that does not use templates at some point. Templates are central to C++ since Alexandrescu's book and the main point around which the current language revolves.
Template meta-programming is a different usage from template generics. It has a higher compile time cost because it performs computations as a side-effect of compilation.
Which is what the commenter above was trying to say, templates are completely normal, vanilla C++. Also, I cannot think of any codebase that is actively under development and doesn't use some template magic, like at least tuples or variadic perfect forwarding etc... If you look at old codebases you can find them, but template metaprogramming is still very common, mainstream C++.
That makes sense. Sounds like the kind of comments you tend to see are out of context and don’t engage with the subject matter. I can see how that comes across as signaling.
The concept of smalltalk are implemented to some degree in many dynamic languages like Ruby, Python, Perl, Elixir, etc.
And if we are going to talk about functional programming, we can't exclude languages like Python for having too small a user base, as their popularity and impact is often wider than that of any static functional language.
> It seems to assume the person in question has no intelligent reason behind their thoughts/opinions.
If they had, they wouldn't systematically resort to baseless appeals to authority, whose only purpose is to intentionally avoid having to present any semblant of an intelligent reason.
> If they had, they wouldn't systematically resort to baseless appeals to authority, whose only purpose is to intentionally avoid having to present any semblant of an intelligent reason.
This is pretty much what I was talking about. It assumes the worst about someone before hearing them out.
It’s a pretty unreasonable assumption to count every reference to Alan Kay’s quote about OOP as a “baseless appeal to authority”.
Perhaps the person has given a lot of deep thought about the matter, and have reached some reasonable conclusions. If you dismiss the person, you’re just cheating yourself of an opportunity to learn and engage in intelligent conversation. And blindly insulting the person.
Definitions arise in real world use, independently of who might have first invented a concept or coined the term.
In the end, only how a term is used by the majority of people matters -- because only that retains the basic requirement of language: to say something and have it be understood.
Sure, misunderstanding happen, true. But at a certain point it becomes just another excuse.
And that's not to get into the "SOLID" definitions which are more a feel-good word for handwave and murky definitions typical of the "True Scotsman" fallacy
How does dropping a bunch of out of context quotes contribite to a discussion? Why was this voted to the top? Clearly I should just skip the first comment tree entirely when reading comments on pieces about "programming paradigms."
> Abstract data type inheritance is about substitution: this thing behaves in all the ways that thing does and has this behaviour (this is the Liskov substitution principle)
There is no such thing as “inheritance” for abstract data types, because inheritance is a syntactic relation between two definitions (which needn't be type defintions, by the way!). Abstract data types may be related by subtyping, although IMO this is hardly ever useful. What you actually want to refine is abstractions (of which abstract types are merely constituent parts).
> As a type, this relationship is reversed: you can use a rectangle everywhere you can use a square (by having a rectangle with the same width and height), but you cannot use a square everywhere you can use a rectangle (for example, you can’t give it a different width and height). Notice that this is incompatibility between the inheritance directions of the geometric properties and the abstract data type properties of squares and rectangles; two dimensions which are completely unrelated to each other and indeed to any form of software implementation.
There are valid arguments against inheritance, but this is not one. In fact, this argument has nothing to do with inheritance at all! All that this says is “if the type variable T only appears in contravariant position in F(T), and A is a subtype of B, then F(B) is a subtype of F(A)”. Moreover, this is not an argument against subtyping either. It's just the description of how to handle it correctly. It appears in any standard PL semantics or type theory textbook.
Here is a real argument against inheritance: “Try formalizing the semantics of a toy language with inheritance. Try proving things about a couple toy programs using this semantics. Note how the proofs about what your superclasses do often make a lot of unwarranted assumptions about how subclasses will use or override inherited functionality. These assumptions couple not only the interfaces (which is okay), but also the implementations (which is not okay) of superclasses and subclasses. This is the antithesis of modularity.”
I think inheritance make code more coupled, which make it hard to delete or refactor. Most of the cases it's better to just copy and paste. You can still share methods. But the moment you need to modify a shared method just so it can be reused: Make a new function instead. One popular solution is to only let functions do one thing but that leads to even more coupling and less reuse.
There's a fine balance between reuse and coupling. Where reuse is good and coupling is bad. A general rule for when to reuse/share/modularize is when the method/function can be reused in other code base. eg. when it can be/is decoupled.
If we are doing OO programming, and we have rectangles, IsSquare would be a good method for a Rectangle whose dimensions are mutable rather than Square being a subclass of Rectangle.
Now you have to remember to check (and repeatedly check) IsSquare at runtime to somehow deal with rectangles that aren't squares, which probably means throwing an exception that someone else has to remember to catch or face runtime errors. But if the dimensions are mutable, no alternative is very satisfactory.
The conclusion 'separate out implementation inheritance' rather weakens the claim 'multiple inheritance of implementation isn't useful'. Because then pragmatics can be allowed to dominate.
We do need more powerful vocabulary for composing implementation (eg, to specify object layout spread non-locally in memory). But multiple inheritance can be valuable, and gets a lot of low-quality criticism.
I'm not convinced the sqr rect example is the best one, at least to say inheritance is wrong. I think this is an example of how not to use inheritance. In my mind the base class should be shape (not rect) or four-sided shape, or whatever the name is for shapes with non-curved sides.
so squares and circles ought to have the same 2D parent, since given the x & y, its just a matter of applying the right transform.
y = ax^2 + bx
is a parabola through the origin.
y = bx
is a line through the origin.
so lines and parabolas ought to have a common 1D curve as a parent, because a parabola reduces to a line when a=0.
clearly a line shifted by a constant is still a line.
so also, a line rotated clockwise or anti clockwise through say 90 degree is still a line.
so then i take a horizontal line, shift it once and rotate it twice, join these 4 lines and get a square.
so squares ought to be a container type with 4 lines ? or wait, if i collapse the two verticals ie. set the height to 0, then a square is just a straight line with no height, which is absolutely true geometrically, but think about the damage you’d do to your type-theory.
but didn’t we just say lines are just degenerate parabolas. so then we ought to be able to take a square, grab its 4 sides which are lines, transformed each line into a parabola, and tell me what that gives you.
and you haven’t even gotten me started on activation functions. since all logistics are richards curves, a gompertz is just a particular richards. so then a relu should inherit from a gompertz and a swish should be a child of relu and somewhere in here we need the hyperbolic tangent, either as a parent or a child or a sibling or the notorious c++ friend function.
now go ask your friendly neighborhood tensorflow colleague why the activations don’t share a common parent, why lines aren’t parabolas and squares aren’t lines and so in and do forth.
IMO this is one of the primary reasons many modern languages not not strictly Object Oriented. They usually support objects to a limited extent, but a certainly not OO like the 90s languages.
> Inheritance was never a problem: trying to use the same tree for three different concepts was the problem.
I wonder why the author describes Polymorphism[1], but doesn't mention the term in any way. At the same time he uses the word 'abstract', but in a different way than any OO/Smalltalk programmer would do. While he mentions the 'Smalltalk blue book', I somehow do not trust his expertise in the field.
> At the same time he uses the word 'abstract', but in a different way than any OO/Smalltalk programmer would do.
I doubt most people who define themselves as OO programmers, eg. working in C#/Java/C++/Python/etc... consider themselves on the Smalltalk side (eg message-passing) of the OO debate. 95% of actual programming experience in object-oriented languages is in languages of SIMULA descent.
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:
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:
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
In practical terms, away from the nebulous confusion, object orientation means that the actual function to call must first be looked up in the first argument of the function to execute:
x.f(y,z);
<==>
F=ooLookup(x,f); F(x,y,z);
This scheme does indeed allow for polymorphism, but I wonder why we would shoehorn polymorphism into a situation, unless it naturally emerges throughout the programming effort as really needed?
In my opinion, functions should not be polymorphic by default, simply, because there is no reason to complicate things unless these complications truly solve a problem.
Inheritance is then a next complication in which F=ooLookup(x,f) recursively tries to resolve the function from a hierarchical class data structure:
x->class->functions
x->class->class->functions
...
This is even a worse complication, which is even more unlikely to be useful.
> There are three different types of inheritance going on.
> 1. Ontological inheritance is about specialisation: this thing is a specific variety of that thing (a [soccer ball] is a sphere and it has this radius)
> 2. Abstract data type inheritance is about substitution: this thing behaves in all the ways that thing does and has this behaviour (this is the Liskov substitution principle)
> A common counterexample to OO inheritance is the relationship between a square and a rectangle. Geometrically, a square is a specialisation of a rectangle: every square is a rectangle, not every rectangle is a square. For all s in Squares, s is a Rectangle and width of s is equal to height of s. As a type, this relationship is reversed: you can use a rectangle everywhere you can use a square (by having a rectangle with the same width and height), but you cannot use a square everywhere you can use a rectangle (for example, you can’t give it a different width and height).
> Notice that this is incompatibility between the inheritance directions of the geometric properties and the abstract data type properties of squares and rectangles; two dimensions which are completely unrelated to each other
By the definitions given at the beginning, where the "abstract data type properties" of an object are its properties as considered by the Liskov substitution principle, this is nonsense.
Obviously, according to this definition, any subtype S of type T which satisfies the article's definition 1 will also be Liskov substitutable for T and therefore satisfy the article's definition 2 as well. This is as far from being "unrelated concepts" as you can get; the one requires the other.
The example with Squares and Rectangles doesn't make sense either. If you have code that is set up to handle Rectangles, and you give that code a Square, then the code will handle the Square correctly. This is because Squares are Liskov substitutable for Rectangles... which is because Squares are, platonically, a kind of Rectangle.
It looks like the author wants to ask "if I encounter an interface that processes Rectangles, and I can only construct Squares, can I achieve everything, by using Squares, that someone else could have achieved by using Rectangles?" But that is not the Liskov substitution principle, or any sort of type theory principle. That would be a principle of computational equivalence.
A common example that is used is when you have a Rectangle.setSize(int width, int height) method. When you make Square a subclass of Rectange, the Liskov substitution principle is violated. I think this (well-known) example is what the author of the article wants to refer to.
This method is of course of type void. The problem is that by calling setSize, there is an implicit return type: the type of the object. This is assumed to be Square because this is the type of the object, but in reality the setSize method takes a Rectangle and produces a Rectangle.
If you would have a method stretch: Rectangle -> Rectangle, the Liskov substitution principle wouldn't be violated: you could substitute a Square for a Rectangle and everything would work the way it's supposed to.
This is because by accepting data after construction with setSize, Rectangle is now invariant as a type. Immutable container types are covariant, because data only comes out. So you can apply LSP really only if you are willing to accept immutability.
> A common example that is used is when you have a Rectangle.setSize(int width, int height) method.
Isn't the fact that we defined a method that changes both sizes at the same time the problem in this case? I mean, if we had two independent methods like `setBase(int)` and `setHeight(int)` this wouldn't be a problem at all (with the assumption, of course, that you accept the fact that the base changes when you change the height).
Am I creating a different class of problems this way?
Also, this seem rather specific to rectangles and squares. Do real-world objects (think `List` in Java) have this kind of problems at all?
The same issue exists for Java Lists. Java lets you pass in subtypes of the type you specified in the List ie it treats them covariantly. This would be correct if the List was immutable but, as you can write to the List, you can blow the type relationship at runtime. In theory a List which you can both read and write should be invariant in the type parameter but this is useless in practice. So Java gives you a useful construct that isn’t completely type sound.
I wasn't talking about generics, I was talking about the `List` interface and its implementations (`ArrayList` and co). Don't they provide a common interface that doesn't leak like the rectangle/square example does?
Also, I posed a question asking if I'm wrong in thinking something, was a downvote really warranted? (Not you specifically, but whoever downvoted)
Java APIs are full of examples like that, with subtypes violating LSP by throwing UnsupportedOperationExceptions from methods that they cannot support.
Many rectangle APIs, even in OO languages, are immutable, so setting width and length isn’t allowed anyways, all you can do is operate on it to get a new one.
I’ve rarely seen a square subtyped as a rectangle, let alone a mutable one. But I guess it’s possible if the mutable and immutable APIs are divided, so that squares are rectangles in its immutable sense but not in its mutable one.
This also pops up in modeling sets. You can define an immutable set by its explicit list of members (intrinsic) or by function that tests membership (extrinsic). If you want to have only one unified type Set that permits both methods of construction, it has to be invariant with respect to the type of its members. This is because the intrinsic approach is covariant (just as a list is) but the extrinsic approach is contravariant (just as a function is in its inputs).
I wish I had a really clear example of this, but it escapes me right now.
Try calculating the diagonal length of your rectangle, where that length is, by definition, 1.414 * width.
Alternatively, notice that if your definition of a "rectangle" includes the ability to arbitrarily modify its own width and/or height, then squares are not Liskov substitutable for rectangles and also aren't a kind of rectangle in the "meaning" sense given by definition 1 above.
Going by the Liskov criterion, a sentient rectangle can double its own width and still be a rectangle, but a sentient square cannot double its own width and still be a square.
On a square either setting the height immediately also sets the width to the same value OR all the types involved are immutable OR you have actively chosen to make a subtype that doesn't properly allow substitution, which is bad.
I think this really shows the point the author struggles with: It's not that OO is flawed because inheritance like this is somehow not possible. It's a paradigm that is deceptively easy but is really very hard to get right. You need to keep these things in mind all the time. The simplest thing to start with is just make everything immutable if you can afford it.
I used to feel like this before writing big and complex programs. But in term of practicality, it's a whole lot easier to just write the expansion of "rectangle" to "square" than re-writing "square" from scratch, essentially copying the majority of "rectangle" properties and functionality.
> This is because Squares are Liskov substitutable for Rectangles... which is because Squares are, platonically, a kind of Rectangle.
> If the type system is sound and expressive enough, ontological inheritance ( this thing is a specific variety of that thing) and abstract data type inheritance (this thing behaves in all the ways that thing does and has this behaviour) should be essentially the same thing.
The difference is that ontology exists in the mind of the programmer, but Liskov substitutability is a property of the program itself. No matter how you model it, a Square is "platonically a kind of" Rectangle. But in order for them to be Liskov substitutable, you have to model them in compatible ways. If my Square class only has a sideLength method, I can't substitute it for a Rectangle.
This simple example may seem silly, but as models get more complex, it becomes harder to make them compatible, even if one modeled class of objects seems like "platonically a kind of" another class. You see this kind of thing all the time in real-world systems. For example, in a UI system, an "OpenGL View" is conceptually a kind of "View", and this relationship is modeled by making OpenGLView a subclass of View. Normal 2D drawing doesn't work in this kind of view, however, so it's not Liskov substitutable.