This is related to the PECS principle in generics:
class List<E> {
appendToMyself(List<? extends E> producer);
copyMyselfTo(List<? super E> consumer);
}If so , is that the same as if I suggested that calling the Rectangle method `setWidth` on a Square should return a Rectangle (that is _possibly_ a Square)?
i.e.
val mySquare: Square = new Square(20)
val myMutatedSquare: Rectangle = mySquare.setWidth(40) // 20 x 40Object of type (a) have only getters in their public API. Setters imply type (b).
And, while each square is a rectangle, the set of all possible square is smaller than the set of all possible rectangles.
To see this, a square can be described by a single number, putting the set of squares in 1 to 1 correspondence with the real number line. A rectangle can be described by two numbers, so the set of rectangles is in 1 to 1 correspondence with the plane.
The line and the plane have the same cardinality, so this means the sets have the same size. For example: https://math.stackexchange.com/questions/243590/bijection-fr...
For observers: fix the lower-left corner of your rectangles at the origin. Then a rectangle is determined by the position of its upper-right corner within the upper-right quadrant. The set of all rectangles is given by the entire quadrant. The set of all squares is the 45-degree line from the origin cutting the quadrant in half. The natural measure on a set of points in this space is "area". The entire quadrant has infinite area, but no matter how infinite in extent the line is, it has zero area. (In the context of probability, of which measure theory is an application, the likelihood of throwing a dart at the upper-right quadrant and hitting the line of squares is nil.)
(You might argue that my choice of origin is "not natural", but it gives the same scheme as the natural "rectangle is (width, length) pair" and is a little more graphically intuitive. Every choice of corner as origin would give an isomorphic setup, resulting in the same subset-zero argument.)
Of course, no such squares actually exist in the real world :) In computers, you're dealing with integers or floating point, and in them the set of rectangles / squares have the same measure.
You could convert any two arbitrary-length datatypes into one, though, with a suitable encoding.
Compare: new Square(side) new Rectangle(width, height)
In this way, a Rectangle is more like a child of a square: widening its capabilities.
Yes, but this will violate the Liskov Substitution Principle, which (effectively) states that any program that is correct for a class (Square) should be correct for the subclass (Rectangle).
So a program that takes in a parent class (Square) can rely on the sides being equal. Now pass in a child class (Rectangle) and your program may break. Hence the violation.
Square(side) : Rectangle(side, side)
There's not a well-behaved way to go in the other direction. Rectangle(width, height) : Square(width) // ???
Rectangle(width, height) : Square(height) // ???That's precisely the problem. Subclasses have to less restrictive than their parents. If a subclass adds observable restrictions, it fails the LSP.
Subclasses can really only add orthogonal properties, such that influencing one of these properties doesn't perturb properties observable from the super-interface.
That's not a given. If the new capability is only in new methods of the subclass, then the program expecting the parent class will never be impacted by it.
To be fair, I suppose you agree:
> such that influencing one of these properties doesn't perturb properties observable from the super-interface.
It's worth noting that even if this capability can only occur due to methods on the subclass alone, it can still fail LSP if those manipulations are still observable to a client using only the superclass.
This is pretty much only possible because multiple clients can have a reference to the same object, since it allows one client to observably transition the object into a state that should be unreachable from the perspective of the second client. (This is the root of the need to make "defensive copies" [1].)
If one imposes a strict single-ownership discipline, then it doesn't matter if super-interface properties can be violated by subclass-only methods, because for the duration of its use via its super-interface, those violations are impossible (and hence unobservable).
If you know you are constructing a Rectangle, then you use it's constructor. If you create a Square, you use that constructor.
But they are not inherited, and can have completely different semantics - accept different arguments or throw for for new reasons.
That having said, the Rectangle / Square example doesn't really work: Is `new Rectangle(42, 42)` a Square? (This brings us to factory methods...)
This is not just abstract... I forget where in the docs TypeScript starts talking about certain things being incorrect because other things are not contravariant/covariant/invariant, but yeah: TypeScript is trying to implement a practical subtyping system and technically subclasses are not subtypes of their parent classes in TypeScript’s original vision, so pragmatism forces you to say “just pretend they are!” and technically the subtype system is unsound.
The problem is that in TypeScript’s theory a type is just the full interface of object properties on a value, including all their methods. Methods always look like functions with an implicit "this" argument type. So this becomes a problem precisely because the type of Rectangle looks like
type Rectangle = {
height: number,
width: number,
someMethod: (this: Rectangle) => something
}
And that recursive definition in terms of having those methods makes a type technically terminal (no subtypes can exist).So it is precisely that we can overload methods with implementations more specific to the subtype, which stops the subclass from being a subtype of the superclass.