1. 35
  1.  

    1. 7

      What does it mean when we break the principle?

      Java has an immutable List<T> that extends from List<T>, but throws an exception when you try to call add (or any other method that would mutate the list). Does this mean that the immutable list isn’t a subtype of List, it just happens to inherit from List?

      1. 6

        Yes, exactly.

        Though arguably, Java’s method contract implicitly includes “Also, the method may fail for any reason at any time.”

    2. 5

      (Disclaimer: I will purposefully conflate subtyping and inheritance, because this is what most programmers are used to.)

      The Liskov’s substitution principle isn’t something you can choose to violate. It holds whether you want it or not, because it’s a tautology, and an obvious one at that:

      Let S be a subtype of T. If P(x) is a property that holds whenever x is a value of type T, then P(x) also holds whenever x is a value of type S.

      So what’s the problem? When the LSP seems not to hold, what’s actually happening is that your types don’t mean what you think they mean. For example, if a class X has an overridable method foo(), then whatever happens in the internal implementation of X.foo() is NOT the definitive semantics of X.foo(). Instead, the correct meaning is that anything could happen at all.

      You can make the meaning of X.foo() more precise by controlling who’s allowed to inherit from X. For example, if X belongs to a package P and your language allows you to prevent classes outside of P from inheriting from X, then, from the point of view of a user of P, the meaning of X.foo() is completely determined by the set of classes in P that do, in fact, inherit from X.

      But, since the set of classes in P that inherit from X is completely known to P‘s author, wouldn’t it be simpler and better to turn X into a sum type, and turn X’s subclasses into the constructors of this sum?

      1. 8

        Conflating subtyping and inheritance is a mistake here, because the point of Liskov’s paper was to define when inheritance is subtyping. If Y inherits from X and Y.foo() breaks any of the LSP criteria, then Y is not a subtype of X. LSP as a principle, then, is “make sure all your inherited classes are also subtypes.”

        1. 1

          Suppose you have a class Stack with non-final methods push() and pop(), whose implementation does what the names suggest. It might be tempting to prove that the following assertion never fails:

          Stack stack = new Stack();
          stack.push(2);
          assert( stack.pop() == 2 );
          

          However, if you program logic lets you do this, then it’s unsound. The problem is precisely that I can define another class FunnyStack that inherits from Stack and overrides push() and pop() to do something else. The correct thing to say is that the type Stack satisfies no nontrivial invariants concerning the behavior of push() and pop().

          1. 6

            I think you’re missing the point: subclassing and subtyping are not the same thing, even if you want to conflate them. Subclassing is one means by which to subtype. Standard ML’s functors are another. Defining different .c files to back the same .h file and linking them into different programs is another. Subtyping is about the mathematical intention of what we’re doing.

            1. 1

              What exactly do you mean by “mathematical intention”? The compiler doesn’t care about your intention.

              1. 3

                The compiler has nothing to do with this. When we write a program, we have an intention about what its correct behavior should be. If we produce a subclass that does satisfy Liskov’s principle, that may be a conscious choice on our part or a mistake. The compiler has no way of knowing. The difference is in what we were trying to do.

                1. 1

                  If we produce a subclass that does satisfy Liskov’s principle

                  “doesn’t”, perhaps?

                  1. 1

                    Yes, thanks.

          2. 1

            Or alternatively, FunnyStack is not a valid subtype of Stack? Not everything that your language lets you define is valid in any particular sense, after all!

            1. 1

              Well, my point of view is that the language’s semantics is the ultimate arbiter of the meaning of your code. So you should either (a) use the language correctly to express exactly what you mean or, if that’s impossible, then (b) switch to a language that lets you express what you mean.

              FunnyStack isn’t a valid subtype of Stack because I didn’t want it to be” is the programming equivalent of “I’m not guilty because I didn’t want to commit a crime”.

              1. 2

                To me it seems more like the equivalent of add(x, y) = x * y. Just because you can write it doesn’t make it correct!

                1. 1

                  A program is neither correct nor incorrect in isolation. That depends on the specification!

                  1. 2

                    Well, I agree. And the Liskov substitution principle is a (partial) specification, and a program can violate that partial specification or not.

      2. 5

        Instead, the correct meaning is that anything could happen at all.

        But since this is not useful at all to define things like this then we need some guiding principles about how to properly write a substitutions…

        But, since the set of classes in P that inherit from X is completely known to P‘s author

        It’s definitely not in the general case. Example: any kind of plug-in system where you inherit from a base class provided by the plug-in API - hell, it could be written in a way that allows the plugin to be written in a completely different PL than the host software.

      3. 4

        Instead, the correct meaning is that anything could happen at all.

        Not quite: the correct meaning is that anything could happen… that consumes no parameters and returns no values. Subclasses are required to conform to the parameter contract of the method, and that’s all in guarantees (interface contracts aside) that typesystems give you anyways.

        But, since the set of classes in P that inherit from X is completely known to P‘s author

        Also, libraries exist.

    3. 5

      All the various various cases can all be explained with variance.

      • If you modify a precondition, that’s input, so it needs to be contravariant (e.g., you can only lift restrictions, not add new ones);
      • If you modify a postcondition / invariant, that’s output, so it needs to be covariant (e.g., you can only add new restrictions, not lift older ones).

      There are cases that can’t be explained by function signatures alone, but that’s because, when you have state, those functions depend on each other, so classes have protocols of utilization.

      For example:

       interface Iterator<A> {
         boolean hasNext();
         A getNext();
       }
      

      Let’s say that we invent a new interface:

       interface DisposableIterator<A> extends Iterator<A> {
         boolean hasNext();
         A getNext();
         void dispose();
       }
      

      This breaks the “Liskov substitution principle” because hasNext and next are no longer the only methods you need to care about to operate Iterator. And if you don’t use dispose, you’ll probably have a resource leak. Therefore all functions working on Iterator are buggy when used with a DisposableIterator.

    4. 2

      This is astonishing if true. It is extremely restrictive on the valid subtypes. For example ButtonWithRedBorder cannot be a subtype of Button! That means essentially every GUI library is Liskov-violating. By contrast, Button and ButtonWithRedBorder can both be implementations of ButtonInterface. Perhaps this explains why I’ve always found it much more satisfactory to designing programs using type classes than objects.

    5. 1

      Under “The fix” you are testing whether WrappedCounter is a subtype of Counter. But earlier wasn’t it the other way around?

      1. 2

        Missed that in editing. Should be fixed now!

    6. 1

      Wouldn’t both the “invariant rule” and the “history rule” follow from the pre- and post-condition rules if we treat mutable state as input/output (as in Haskell)?

      1. 1

        Not necessarily! Consider the property “calling pop right after push leaves the stack unchanged.” That’s covered under the history rule but can’t be covered by mutable contacts.

    7. 0

      SOLID is a stupid idea. Especially the L part. Apparently someone was looking for what an L can stand for to have a nice abbr. Think, have you ever worked on something what does not comply with this “principle”?

      1. 1

        Where in the article do I say anything about SOLID?

    8. [Comment removed by author]