The question is ill-defined as it stands, thus the disagreement between the answers.
First of all, talking about something like this within Python is a bit like debating what sharpening technique works best on butter knifes, and then you also don't specify what int
means to you, in its role as a superclass.
Let's make that clearer by phrasing it all in C# and with a bespoke superclass. If it is something like the following:
public class Int {
protected int i;
public Int(): {this.i = 0;} -- zero constructor
public virtual Int operator+(Int other) {
Int result;
result.i = this.i + other.i;
return result;
};
public virtual Int operator-(Int other) {
/* similar */ }
(operator+
is the C++ / C# way of defining an addition operator that you'd use like i + j
)
then you could have a subclass
public class PositiveInt: Int {
public override Int operator+(Int other) {
Int result;
result.i = this.i + other.i;
return result;
}
Int operator-(Int other) override { ... }
(the override could just have been omitted, it's the same as in Int
). Then this does not violate LSP – PositiveInt
behaves just like any other Int
as far as someone accepting such a value is concerned.
But that's not a particularly useful interface. In particular, notice that adding together two PositiveInt
values gives you something of type Int
. It would be far more sensible for the result to be again a PositiveInt
. It can be done in C#, but you need to inspect the other
argument to check whether it's a PositiveInt
as well, which is kind of violating the sporit of OO.
public class PositiveInt: Int {
public override Int operator+(Int other) {
if (other is PositiveInt oPos) {
PositiveInt result;
result.i = this.i + other.i;
return result;
} else {
Int result;
result.i = this.i + other.i;
return result;
}
}
Int operator-(Int other) override { ... }
Even then, the fact that positive+positive=positive isn't in any way obvious from the interface. Doing that is actually rather awkward to express in OO languages, but we could do it by making the addition operator an in-place addition, so the result is forced to always keep the type of the subclass:
public class Int {
protected int i;
public Int() {this.i=0;} -- zero constructor
public virtual void operator+=(Int other) {
this.i += other.i;
};
Problem is: now this completely breaks for the subtraction operator, because even subtracting two postive numbers does in general not give you a positive number!
That's just for the specific example of positive numbers – for other special-numbers you'd get other discrepancies.
In summary, you either lose the expressivity of the type, or violate the contract in the subclass. In C# and moreso in Python you could get around these issues by changing the result type ad-hoc: the subclass +
operator could just always return a PositiveInt
, whereas the -
operator could return a PostiveInt
in case the other
number is smaller than the self
one. But at that point we've just given up on talking about class interfaces and have a messy mix of isinstance
checks and duck-typing.
What to make of it? Well, I'd say it just doesn't make sense to use Int as a superclass. You should instead have abstract superclasses expressing just what mathematical operations you want to have and their types, and then subclass them for concrete implementations. This is however quite awkward to do in class-based OO. The Pythonic way would probably be to just not use subclassing for the purpose: simply make PositiveInt
a separate class and rely on duck typing to use it with existing code. I personally don't like that because it's very easy for code to break when it turns out people made different assumptions about the quacking protocol, but it can definitely work as long as you're disciplined with unit tests.
Much preferrable IMO is to use a language that can properly encode the maths. If you're really serious about it that means you need something like Coq, but you can also get reasonably close with the much more accessible Haskell. In that language, classes (typeclasses) are always just abstract interfaces:
class AdditiveSemigroup g where
(+) :: g -> g -> g
This type signature expresses that the result will have the same type as the operands – adding two Int
s gives you an Int
, adding two PositiveInts
gives you a PositiveInt
, etc..
instance AdditiveSemigroup Int where
p + q = p Prelude.+ q
newtype PositiveInt = PositiveInt { getPositiveInt :: Int }
instance AdditiveSemigroup PositiveInt where
PositiveInt p + PositiveInt q = PositiveInt (p Prelude.+ q)
Then you have a stronger class that also adds subtraction, but still closed within the type. This can be instantiated for Int
, but not for PositiveInt
:
class AdditiveSemigroup g => AdditiveGroup g where -- l=>r means r is a subclass of l
zero :: g
(-) :: g -> g -> g
instance AdditiveGroup Int where
zero = 0
p - q = p Prelude.- q
To also have a subtraction but with different type of the result, you'd have yet another class:
instance AdditiveMonoid g => MetricSpace g where
type Distance g
(.-.) :: g -> g -> Distance g
instance MetricSpace PositiveInt where
type Distance PositiveInt = Int
PositiveInt p .-. PositiveInt q = p Prelude.- q
PositiveInteger(2) - PositiveInteger(5)
not to throw. Interestingly, I'm seeing a link with maths: (int
,+
) is a group, while (PositiveInteger
,+
) is only a monoid