2

I've recently become incredibly confused about the notion of "strengthening/weakening a precondition/postcondition". I think my confusion lies in my interpretation of the words "precondition" and "postcondition". Here is why I'm confused:

In the rules of Hoare Logic, one can "strengthen the precondition and/or weaken the postcondition", as per this question.

However, according to LSP, one can only "weaken the precondition and/or strengthen the postcondition", as per this question.

I attempted to work through it myself, and I convinced myself that you can "strengthen the precondition and weaken the postcondition", but I did so using LSP (sorta). To do so, I considered a function Y f(C c) that takes an object of type C and returns an object of type Y. Now, C inherits from B, and D inherits from C, and Y inherits from X and Z inherits from Y (Z : Y : X and D : C : B).

To strengthen the pre/post-condition would be, in my mind, to replace C with D and Y with Z. My rationale is that a subtype is "stricter" than its parent because it satisfies every condition its parent satisfies plus some extra conditions. To weaken the pre/post-condition would be to replace C with B and Y with X. The parent type is "weaker" than the subtype because it has to satisfy "less" conditions than the subtype.

Now, it appears that we can "strengthen" the precondition without failure, because by LSP if f works with an object of type C then it must work if we replace C with a subtype of C (i.e. D). This makes sense to me because D satisfies everything C satisfies, but more, whereas B may not satisfy everything C satisfies, and thus may not satisfy every condition f needs to function properly. Hence, I have "shown" that we can strengthen the precondition, and cannot weaken it.

As for the postcondition, it seems to me that we can only strengthen it. If f were to return an object of type X, this would be totally fine, since an object of type Y is also an object of type X. However, f cannot necessarily return a Z. My counterexample to demonstrate this is the inheritance tree Rect : Shape and Circle : Shape. If f returns a Shape, that Shape may not necessarily be a Rect, it may be a Circle, or it may be neither. Hence we can't strengthen the postcondition.

In summary, I demonstrated to myself that:

  • You can strengthen the precondition and weaken the postcondition.

Obviously this doesn't comply with the second question above. Apparently I violated LSP. Is it in my definition of "strengthen" and "weaken", or my idea of "precondition" and "postcondition", or my analogy? Where is the fault in my understanding? Are the ideas of Hoare Logic and LSP simply incompatible?

2
  • Hoare Logic is a way to prove correctness; Liskov substitution is a way to prove that system invariants, previously proven, are still valid when one functionally equivalent component is substituted for another. Commented Apr 3, 2017 at 23:56
  • Liskov substitution only applies when the preconditions, etc are specified. Simply saying "rectangle" or "shape" is not sufficient to specify the behavior. The names of the classes are not relevant, but rather, exactly what are the specifications. Keep in mind that Liskov worked with the CLU language, and formal specifications were a key part of her work. Commented Apr 3, 2017 at 23:58

2 Answers 2

3

To truly understand how strengthening pre-conditions and weakening post-conditions is a violation of the principle Barbara Liskov has published, it is the best to actually look at a practical example.

For the demonstration we will need a few classes interacting with each other.


First we have a ValidParent class, which sets some rules about a negate. The rules are for the method the following:

  • accepts only positive numbers as its parameter (excluding zero) and throws on an invalid value,
  • negates passed value and returns it - effectively only ever returning a negative number.

Ie. the input parameter MUST be > 0 (pre-condition) and the return value is guaranteed to be < 0 (post-condition).

Besides this the class also contains other method, which does some very cool stuff (for purpose demonstration only writes line to a console).

class ValidParent
{
    public int negate(int positiveNumber)
    {
        if (!(positiveNumberToBeNegated > 0)) {
          throw new Exception("The method only accepts positive numbers.");
        }

        return positiveNumber * -1;
    }

    public void doSomething()
    {
        Console.WriteLine("Otherwise very useful method.");
    }
}

This class alone is not enough to demonstrate the problems with strengthening pre-conditions and weakening post-conditions, for that we will need Another class which uses the ValidParent.

This Another class takes a ValidParent as a dependency and then uses it to conduct some operations in its doOperation method.

Let's imagine you're the programmer of the Another class and you are programming the doOperation method in which you will be using the ValidParent instance. Because of that you need to know how the ValidParent class looks so you look at the documentation and it tells you the following about one of it's methods:

The negate method accepts only positive numbers as its parameter (excluding zero) and throws on an invalid value, negates passed value and returns it - effectively only ever returning a negative number.

With that in mind you know that should the negate method return a value it will always be negative and never will be zero, so you program your doOperation method like this:

class Another
{
    private ValidParent validParent;

    public Another(ValidParent validParent)
    {
        this.validParent = validParent;
    }

    public double doOperation(int positiveDivisor)
    {
        try {
            validParent.doSomething();
            var negated = validParent.negate(positiveDivisor);

            return (double) 1 / negated;
        } catch (Exception ex) {
            Console.WriteLine(
                "You passed an unsupported value to doOperation method." +
                "Value: "+ positiveDivisor.toString() + "." +
                " Method only accepts positive values.";
            );

            return 0;
        }
    }
}

And you are instantiating the Another instance as follows:

new Another(new ValidParent());

You run some tests, passing positive, negative and zero values to the doOperation method and it all works as expected, dividing 1 by a negated passed value when the value is present, otherwise writing out to a console and returning 0. You are happy with your result.


How strengthened pre-condition breaks applications

Some time passes and a new logic is introduced to your system. This logic says, besides supporting positive values, you MUST also have a case where, in a specific place of the program, only positive values greater than 10 are supported by the negate method, ie. you are required to strengthen the pre-condition, because you are narrowing the list of accepted values from all positive numbers to only positive numbers greater than 10.

So you extend the ValidParent and create its child.

class StrengthenedPreConditions extends ValidParent
{
    public int negate(int positiveNumber)
    {
        if (!(positiveNumberToBeNegated > 10)) {
          throw new Exception("The method only accepts positive numbers greater than 10.");
        }

        return positiveNumber * -1;
    }
}

So far your code works well, until a new developer joins your team. This new developer is doing some profiling and during it by human-error changes the following line:

new Another(new ValidParent());

to:

new Another(new StrengthenedPreConditions());

As expected, the code compiles without any problems, but one day you are watching the console and suddenly see very strange messages:

You passed an unsupported value to DoOperation method. Value: 5. Method only accepts positive values.
You passed an unsupported value to DoOperation method. Value: 1. Method only accepts positive values.
You passed an unsupported value to DoOperation method. Value: 10. Method only accepts positive values.

The message really is strange, because all three values, 5, 1 and 10, are in fact positive. You start to inspect where the problem is and locate it within the Another class. You navigate to ValidParent and have no idea what is wrong, because the ValidParent supports positive values, so it should support the three values as well. But then you realize there's a child of the ValidParent class and you find the issue. By strengthening the pre-conditions you broke another class, which was counting on the ValidParent to accept ALL positive values, not just some of them.


How weakened post-condition breaks applications

Another time passes and for some reason a new class finds its way into your system. Once again, this class is a child of the ValidParent and overrides the negate method.

class WeakenedPostCondition extends ValidParent
{
    public int negate(int positiveNumber)
    {
        return 0;
    }
}

The method return 0 and does nothing else. By returning zero, you are weakening the post-condition, which, as stated by the ValidParent is: effectively only ever returning a negative number, but your WeakenedPostCondition class returns 0, effectively returning a value which is not within the initial set of values determined by ValidParent.

Let's see how we've gotten to the point of returning negative values from the negate method first:

  1. A method returns int data type.
  2. A method will never return a positive number - strengthening.
  3. A method will never return a zero - strengthening.
  4. A method now returns only negative numbers.

By returning zero from the child, you are omitting one strengthening operation, thus weakening the post-condition.

As in previous example with strengthened pre-condition, a similar mistake happens in your code at some time, replacing:

new Another(new ValidParent());

with:

new Another(new WeakenedPostCondition());

Once again, the code compiles, but one day you receive an email:

ZeroValueDivisionException thrown by Another::doOperation (Line 16).

You look into the class, once again inspect the ValidParent and see nothing wrong there - there is no possible way the ValidParent::negate would ever return a zero, so how could the ZeroValueDivisionException ever happen? Then you notice the WeakenedPostCondition child and it all clicks.

In this case your application completely crashed because the Another::doOperation method expected the negated value to always be a non-zero thus performing the division (the non-zero value was guaranteed by the ValidParent class), but one of ValidParent's children broke the condition.

1

There is a fundamental flaw in your reasoning: you consider the return type as a precondition.

Let's look at your example. Imagine that C is a color and Y a shape. Now imagine that f is defined so that every warm color returns a triangle, and every other color a circle.

According to LSP, you are allowed to substitute D to C. Let's consider that in our example D is the subtype of C made of the warm colors.

The function f can without problem process the more specialised warm colors, as they behave as more general colors. But you can't replace arbitrarily the return type: the properties of the return values depend on the input values. Otherwise said, the type of the return values is a post condition, not a precondition.

Back to our example: the LSP on the input type implied that the output type is triangle shape. It's not that you could, as precondition, choose the output type with another substituable type. Our valid substitution strengthened the postconditions, as expected.

My example is of course extreme. Depending on how f is defined, you could as well not narrow down the output type at all, and just infer some stronger postconditions.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.