Barbara Liskov recently won the Turing award. It’s great to see the importance of programming language work recognized once again. Even if you aren’t into the academic literature, you have probably heard her name, for example, when people mention the Liskov substitution principle (LSP). The LSP states that if a property P is provable for an object of type S, and T is a subtype of S, then P is provable for an object of type T.
This is behavioral subtyping - instances of subtypes are semantically substitutable where supertypes are expected. This is a very strong requirement, but not something a practical programming language can enforce.
Closely related (and enforceable) is the subsumption rule of type systems for OO languages. This rule states that if an expression e has type T, and T is a subtype of S, then e has type S as well. This post is about the importance of subsumption in programming language design.
The subsumption rule is a cornerstone of formal type systems for OO. More importantly, it captures a deep intuition that programmers have about subtyping. After all, what could be more natural: if e: T and T <: S, e: S. I hope readers agree with me that this property is intuitive, perhaps even so obvious that you wonder why anyone would bother belaboring the point. *** So it is most unfortunate that mainstream programming languages violate subsumption left and right. Below, we’ll look at some examples, and draw some conclusions about language design. Case 1: Hiding fields. In Java (and C++) you can define a field in a subclass with type X, even though the superclass has a field of the same name, with unrelated type Y. The subclass’ field hides the superclass field.
class S { final int f = 42;}
class T extends S { final String f = “!”;}
new T().f; // “!”
((S) new T()).f; // 42
In case it isn’t obvious, any object of type S has the property that it’s f field has type int. An instance of T should also have the same property, but doesn’t. To get at the field defined by S, we have to coerce the T value into an S.
Tangent: It would be much better to ensure that fields are always private, or better yet, avoid field references altogether, as in Self or Newspeak.
A typical programmer in the Java/C++ tradition may ask what’s the harm in the rules as they are.
The harm is precisely in having rules that contradict strong and valid intuition. The contradiction will bite you in the end, because you will end up thinking some program property holds based on your intuition, while the rules of the language contradict that intuition. Years of exposure to these misfeatures may have inured the mind to their toxicity, but they are toxic all the same.
However, there is much more harm, as we’ll see as this post progresses.
Case 2: Hiding static methods. The same sort of situation occurs with static methods. Static methods aren’t really methods at all, as no run time object is involved in determining the meaning of their invocations. No overriding can occur between static methods. The meaning of a static method is statically bound: static method is a misnomer for subroutine (as in FORTRAN subroutine circa 1955). And so Java allows static methods to hide each other much like fields. You can easily recreate the above example with static methods.
I’ve written elsewhere about the problems of all things static; add this to the list. Maybe you’re beginning to get the sense that violating subsumption correlates with iffy language constructs.
Case 3: Package privacy. This brings us back to Barbara Liskov. Her work on CLU in the mid 70s brought David Parnas’ notion of information hiding into programming languages, with support for abstract datatypes (ADTs). Packages are one of the mechanisms that Java uses to support ADTs. Packages have had all kinds of problems; lack of subsumption is at the root of many of them.
For example, recall that in Java, protected access implies package access. Package access is always relative the the current package. So when a protected member is inherited by a subclass in another package, it eliminates the access from the superclass package (while granting access to its own package):
package P1;
public class A {protected int foo(){return 91;}}
class C {static int i = (new P2.B()).foo();}
**
package P2;
public class B extends P1.A {}
This code is legal. If we then change B such that
public class B extends P1.A {protected int foo() {return 42;}}
The call in C is no longer allowed! We have not added or removed members in the subclass, or changed their accessibility (or have we? Did we have a choice?) and yet we’ve broken client code.
I don’t want to go into the details of an even more bizarre example (due, as far as I can recall, to David Chase) - but others have. Suffice to say that there was a huge bug tail around such examples; it took years to resolve them. Compiler engineers did not correctly diagnose the problems, because their intuition led them to try and preserve subsumption, to no avail. I have several patents to my name that deal with the mechanisms of implementing the desired behavior.
And why should you care? Because these issues led to subtle incompatibilities between compilers and VMs, across vendors and releases; these led to subtle program bugs, to jokes about write once debug everywhere etc.
Packages are one way in which Java supports ADTs. It turns out that you cannot have ADTs, inheritance and subsumption in one language. You must make a choice - and I argue that subsumption must always be preserved.
Case 4: Class-based Encapsulation.
Another mechanism that supports ADTs in all the mainstream OO languages is class based encapsulation. This the idea that privacy is per class type, not per object. This makes it possible to write code like
class C {
private int f(){return 42;}
public int foo(C c){return c.f();} // I can get at c’s f(), because we’re both C’s
}
It also makes it possible to violate subsumption:
public class A {
{
((A) new B()).f(); // 42
new B().f(); // “!”
}
private int f(){return 42;} // package private
}
public class B extends A {
public String f(){return “!”;}
}
An alternative to class-based encapsulation is object-based encapsulation. Privacy is per object. You can enforce it via context free syntax. For example, you can arrange that only method invocations on self (aka this) attempt to lookup private methods. You don’t need a typechecker (aka verifier) to prevent malicious parties accessing private methods or fields. Consequently, you don’t have to rely on a complex byte code verifier to prevent such access as one does in Java and .Net.
This fits perfectly with the object-capability security model. It is essentially what we do in Newspeak; but the ideas are universal.
Oh, and object-based encapsulation works with both subsumption and inheritance.
Case 5: Privileged access within “nests” of classes. Giving enclosing classes privileged access to their nested classes, and giving sibling classes privileged access to each other, is another violation of subsumption (but giving a nested class free access to its surrounding scope, including private elements of its enclosing class, is fine).
Another way to look at all this is that object-based encapsulation falls out naturally from the use of interfaces. Consider what would happen if the type C above was, by language definition, the public interface of C (as it is in Strongtalk, for example). Subsumption would fall out automatically.
Subsumption is a very good litmus test for language constructs; if a construct violates subsumption, it will cause grief. Ditch it.
Strict adherence to subsumption reduces cognitive dissonance for programmers, compiler writers, VM implementors and language designers. It helps avoid bugs in user code, compilers and VMs, enhancing reliability and portability. It makes it easier to build secure systems.
So if you design a type system for an OO language, preserve subsumption at all costs. Do not try and combine ADTs with inheritance (because then you lose subsumption).
Now if we can’t have ADTs and inheritance, we have to chose between them. I believe the benefits of inheritance far outweigh the costs. Those who feel differently are unlikely to do better than Luca Cardelli‘s sublimely beautiful Quest. On the other hand, if one choses inheritance, one should go with object-based encapsulation.
Put another way, if you take the adage program to an interface, not an implementation seriously, it will prevent a host of problems. This is what we are doing with Newspeak.
At a meta-level, the lesson is: pay attention to programming language theory; occasionally, you can learn from it.
Update: Yardena pointed out this discussion of Java packages. It goes through the problems in detail. I've added a link to it in the main post as well.
A place to be (re)educated in Newspeak
Sunday, March 29, 2009
Subscribe to:
Post Comments (Atom)
12 comments:
Great post. Here's a relatively recent account of cognitive mismatch between inheritance and package privacy, titled no less than The Ultimate Java Puzzler.
Hi Yardena,
Thanks for the link. A quick scan indicates it goes through much of the brutal detail I was reluctant to spend time/space on. Maybe I'll update the post to link to it.
Of course my emphasis is not on the specific problems of Java packages, but on what underlies them. That is the way to avoid language puzzlers.
I believe the benefits of inheritance far outweigh the costs.
I'm not convinced the benefits of inheritance are really all that compelling. Perhaps you should enumerate these benefits, because they're a lot less clear than the benefits of subsumption.
i was surprised to hear anybody likes inheritance! but i'm only coming from mostly a C++/C#/Java world, not from say the Smalltalk-derived world. i would very much like to hear and learn about your thoughts and experience wrt inheritance. (in and of itself, but also wrt ATDs would be quite interesting.)
thanks for all your efforts, btw. e.g. this post really helps me understand more about what the heck java is doing; things i just sort of accepted but really didn't grok at all.
Sorry if it's something basic, but why can't you have inheritance, ADTs and subsuming? I tried googling but without much luck.
Thanks,
Adam Warski
Adam:
Say you have ADT_S with abstract typpe S, with private feature m of type int. You extend this with ADT_T with abstract type T. T has feature m of type String. No contradiction, as ADT_T cannot see the unexposed feature m of S. Now suppose you add
foo(T t){return t.m;}
to ADT_S. What is the type of the returned value? It seems obvious that it is String. But t also has type S by subsumption, and so one can prove that t.m has type int.
Thanks! It's a bit more clear now.
You've used inheritance and subsumption in the example, but what's the crucial property of ADT's that matters here? Private/public features?
ADT's have a rather vague definition (at least to my knowledge; no doubt there are concrete ones, but these are rather used in specific papers), that's why I'm not sure what you exactly mean when writing "ADT".
Adam
Adam, the key property of ADTs here is privacy PER TYPE.
And isn't privacy/privacy per type just an implementation detail of ADTs?
Adam
No. It's what they are all about. Privacy is achieved by type abstraction (rather than by procedural abstraction).
For the case 1, I find strange that your conclusion is "It would be much better to ensure that fields are always private, or better yet, avoid field references altogether, as in Self or Newspeak.", IMHO what you show is that if you allow this then the language should enforce that T.f's type is a subtype of S.f so that the substition principle is preserved.
renoX:
First of all, since it's a presumably mutable field, it would have to have the exact same type not a subtype.
The attraction of hiding is that adding a field in a superclass does not break the subclass. Once you put any kind of constraint on that process, you lose that property and it is all rather pointless.
Post a Comment