I came in expecting to fully agree with you here, but I think I see a flaw in your logic, unless I'm misunderstanding you.
> Duck typing environments are saner in my opinion because they are really just interfaces. You are not making the strong statement "I am an X" (and thus creating the expectation of static behavior), but instead "I have loose behaviors A,B, and C" (in the same way as interfaces).
I've taken this to mean that you should not subclass unless you can guarantee that the external behavior will be the same, i.e. you will always get the same output for the same input, although its performance characteristics may be different. This contrasts with implementing an interface, where you can return different results, as long as the results are valid. Just trying to paraphrase at this point -- tell me if I'm misrepresenting you.
Where I'm confused is trying to figure out why a violation of the former property would be an issue. If your code block instantiates a subclass, then it knows it isn't dealing with the base class, and it expects the behavior of the subclass -- no problem there. However, when you write a function that asks for the base class as a parameter, that strikes me as a law of demeter violation. The function should not care what the passed object does, as long as it does it legally.
At the risk of straw-manning, I assume your response would be that you should be asking for the interface in that case, not the class. And that is my point. You should never need a particular concrete data type, although you might ask for one to avoid the boilerplate of creating a thousand one-off interfaces.
So this gets back to what people are actually trying to do by subclassing List<T> or Set<T>. They want a good default implementation of IEnumerable<T>, IList<T> or ISet<T>, but with some of the behaviors switched out. They'll still fulfill the base interface requirements, and that's all any other function should ask for, so really, what's the problem?
I knew that sentence would require a much longer and deeper discussion (and I was hoping not to leave OOP to explain it). But in a nutshell, to me interfaces are meant precisely to represent "unknown behavior" and classes for "known behavior".
If we take a second to temporarily forget classes altogether and look at functional programming, you may have something like map(). map() takes a list (well known construct) and an iterator function (unknown outside behavior). The API itself makes it clear what can be changed and what can't. The scope of what is dynamic about the function and what isn't is immediately obvious from the types themselves, completely transparent.
To me, interfaces serve this same role in non-functional programming: they represent outside and dynamic behavior. Classes on the other hand represent known behavior. The problem with "good default implementations" is that they mix these two (opposite) concepts together, which regularly leads to completely "consistent" but absurd outcomes, that are usually attempted to be fixed with language-bandaids. Let me provide two examples.
The first is NSArray and NSMutableArray in Cocoa/Cocoa Touch (apologies for the use of these, it is my strongest background, and ironically enough, one of the closest "correct" subclassing examples in my opinion). NSMutableArray is a subclass of NSArray. This makes no sense. Granted, it makes complete sense from an "implementation" view, but when it comes to user code it makes the classes meaningless: if I make a method that takes an NSArray, the whole point is I'm saying its immutable. But I can pass in a mutable array and none of my expectations will be valid: the array could change right under me. And yet the compiler will be perfectly happy because it is a correct statement, NSMutableArrays are NSArrays. This should theoretically be an edge case: using a subclass to provide the OPPOSITE behavior of the original superclass, rendering the type-system absurd, and yet it is a widely used construct. I am thus forced to defensively copy the immutable array because it may very well be a mutable -- something completely "consistent" in this world. This to me shows this fundamental confusion of interface vs. classes. To get back to what you were saying, the problem with subclassing is precisely that it is incredibly difficult to state what is done "legally".
If you have a pure interface, with no existing "default" behavior, its just like a lambda: anything goes. If you have a class, its static. If you have both (subclassing), it gets incredibly tricky and hard to predict the interaction. The entire API of the class becomes surface area for mutation.
Now let me give you the second example: UIView. UIView has a -subviews method. Why can't I override -subviews to return a static list of views if I never want it to change (thus rendering addSubview: and removeSubview null)? The documentation does NOT list this method as non-overridable (And many people in fact override it to do perfectly legitimate things btw). And if I do override it, I still abide by all the postconditions of the method as defined by the API. And in fact, OTHER parts of the framework totally allow (And sometimes encourage!) this type of "override instead of setting"-style programming. As a novice using the framework, its a completely logical expectation that this should work. So why does UIKit completely break down?
Because the real postconditions are incredibly more complicated than just "Return your subviews". You have part of the code written as if nothing will ever change (internally UIView relies on the _subviews ivar), but another that acts like an interface advertising that it might change (possible UIView subclasses). And half the time, the changes don't upset any delicate balance. But other times, it does. Things like a final keyword wouldn't fix the above problem because again, sometimes its legitimate to override. You can absolutely start writing your code in such a way where things "should" work no matter what method a subclass overrides, but that is very hard to get right (and very hard to test), whereas you get this for free by just forcing everyone to use the external interface! (This is what is meant by breaking encapsulation: you work so hard to provide a sane API, that then anyone can break by changing in otherwise completely legitimate ways). The real problem is that you are saying two contradictory things. What you actually want is to do ONE of the following:
1. Force everyone to use addSubview: and removeSubview: since its a concrete class in a hypothetical non-subclassable Objective-C world. No absurd results because, again, everyone is using the API you worked so hard on and properly tested (i.e. there is no way to futz around with -subviews) OR
2. Have -subviews be a method of some sort of Drawable interface, such that every other piece of code is now known to work with the expectation that it calls that instead of some internal ivar or something, allowing all the shenanigans you want.
Hopefully that makes my views a little clearer, but I'm super tired since its 2AM here so perhaps some of this was more verbose or not as clear as it could be.
> Duck typing environments are saner in my opinion because they are really just interfaces. You are not making the strong statement "I am an X" (and thus creating the expectation of static behavior), but instead "I have loose behaviors A,B, and C" (in the same way as interfaces).
I've taken this to mean that you should not subclass unless you can guarantee that the external behavior will be the same, i.e. you will always get the same output for the same input, although its performance characteristics may be different. This contrasts with implementing an interface, where you can return different results, as long as the results are valid. Just trying to paraphrase at this point -- tell me if I'm misrepresenting you.
Where I'm confused is trying to figure out why a violation of the former property would be an issue. If your code block instantiates a subclass, then it knows it isn't dealing with the base class, and it expects the behavior of the subclass -- no problem there. However, when you write a function that asks for the base class as a parameter, that strikes me as a law of demeter violation. The function should not care what the passed object does, as long as it does it legally.
At the risk of straw-manning, I assume your response would be that you should be asking for the interface in that case, not the class. And that is my point. You should never need a particular concrete data type, although you might ask for one to avoid the boilerplate of creating a thousand one-off interfaces.
So this gets back to what people are actually trying to do by subclassing List<T> or Set<T>. They want a good default implementation of IEnumerable<T>, IList<T> or ISet<T>, but with some of the behaviors switched out. They'll still fulfill the base interface requirements, and that's all any other function should ask for, so really, what's the problem?