3

The Liskov Substitution Principle states:

Invariants of the supertype must be preserved in a subtype.

I'm particularly interested with the intersection of this principle and polymorphism. In particular subtype-polymorphism though, in fact, this seems to be the case with parametric polymorphism and Haskell type classes.

So, I know that functions are subtypes when their arguments are contravariant and their return types covariant. We can assume that methods are just functions with an implicit "self" argument. However, this seems to imply that if a subclass overrides a method of the parent, it is no longer a subtype, as one of it's methods is no longer a subtype.

For example. Take the following pseudo-code:

class Parent:
    count : int
    increment : Parent -> ()
    {
        count += 1
    }

class Child inherits Parent:
    increment : Child -> ()
    {
        count += 2
    }

So going back to the LSP: can we say that a property of Parent.increment() should hold for Child.increment() even though these two don't obey a strict subtyping relation?

More generally my question is: how do the rules of subtyping interface with the more specific arguments of polymorphic functions and what is the correct way of thinking about these two concepts together?

Sam Pattuzzi
  • 31
  • 1
  • 2

2 Answers2

0

Quoting Wikipedia's article on Liskov Substitution Principle

More formally, the Liskov substitution principle (LSP) is a particular definition of a subtyping relation, called (strong) behavioral subtyping [...]

Behavioral subtyping is a stronger notion than typical subtyping of functions defined in type theory, which relies only on the contravariance of argument types and covariance of the return type. Behavioral subtyping is trivially undecidable in general [...]

There are a number of behavioral conditions that the subtype must meet:

  • Preconditions cannot be strengthened in a subtype.
  • Postconditions cannot be weakened in a subtype.
  • Invariants of the supertype must be preserved in a subtype.

Thus LSP is a stronger definition for sub-typing, that relies on features beyond type-theory.

In your example, this rises and falls on your invariant.

calling increment will increase count by **exactly 1**

Obviously Child can't be expressed in terms of Parent since the invariant is broken. This can't be deduced from syntax alone.

LSP should lead you to define Parent and Child separately, having them both inherit from Incrementable which has a weaker post condition.

Sirotnikov
  • 444
  • 4
  • 10
  • My question is more about the semantics of the argument. LSP states that an invariant holds for subtype iff it holds for the supertype. The subtyping relation states that for functions `a -> b <: c -> d` iff `c <: a` and `b <: d`. According to that definition of subtyping, `Child.increment()` is not a sub-type of `Parent.increment()`. Therefore, why should LSP apply? – Sam Pattuzzi Jul 25 '14 at 10:04
  • The correct way of thinking is that LSP is a guideline for when polymorphism makes sense, NOT as a logical /mathematical property of it. Polymorphism supplies you a car. LSP says you should only drive on roads. You're saying "hey, look I'm driving on the sidewalk. LSP is broken". Is it? – Sirotnikov Jul 26 '14 at 18:33
  • My understanding was that Liskov herself gave the principle a very theoretical and mathematical grounding. There are numerous papers written on the matter. So, on these grounds, I disagree with your premise that LSP is just a guideline. – Sam Pattuzzi Jul 28 '14 at 22:22
  • Well, to be fair your question is phrased in a manner which is easily interpreted as philosophical question more than hard core theoretical. But perhaps it is my bias. It's been a while since my type theory lessons, but I never recall the theory being 'proper'. In any case your theoretical question is answered in [Liskov substitution principle](http://en.wikipedia.org/wiki/Liskov_substitution_principle): I'll edit my answer. – Sirotnikov Jul 30 '14 at 01:48
  • Thank you. It wasn't entirely clear from my question. I think this answer is very good. One point to clarify: the type it refers to in the link is that of the *class* and not the method on the class. For example: `increment will increase count` makes no sense as a property of the method because it raises the question, what is count? A corollary to this is that: `Child.increment : Parent -> ()` and not `Child.increment : Child -> ()`. They are in fact one method that behaves differently under different types (logically speaking). – Sam Pattuzzi Jul 30 '14 at 14:59
  • To be honest, I'm used to less formal discussion of those issues, so forgive me if I'm not getting what you mean. I look at `increment will increase count` **not** necessarily as a property of the method, but an as invariant of the class. As such, it has to rely on _inside knowledge_ of the class, since we never define any outside outcome for `int count`. There's no `getCount()` method. Philosophically speaking, if the inner state of `Parent` (ie `int count`) never affects the outside world - is something _really_ broken? – Sirotnikov Jul 31 '14 at 15:33
  • No invariant has been broken here. Only a (possible) postcondition. – Elazar Jul 31 '14 at 15:59
0

The term "subtyping" is technically a syntactic matter. So syntatically, Child <: Parent.

Liskov principle is about behavioral subtyping, as noted in wikipedia. It requires syntactic subtyping, but it also depends on your definition of the class' invariant and pre/post conditions. Since you didn't define any, it is nonsensical to talk about violations.

If you define increment's postcondition as new count = old count + 1, there is a violation.

If you define increment's postcondition as new count > old count, there isn't any.

In general, defining postconditions to be "exactly the parent's postcondition" makes inclusion-polymorphism impossible by defintion. Where polymorphism makes sense, the definition of postcondition should be relaxed.

Note that class invariant is about possible values - snapshot of the object - and since you can define Child's increment in terms of Parent's increment, it cannot violate any invariant.

Elazar
  • 20,415
  • 4
  • 46
  • 67