In this session, we're going to take a closer look at the interactions between subtyping and generics. We've already seen the two principal forms of polymorphism. One was subtyping and the other was generics. In this session, we'll look at the interactions. In fact, there are two main areas of interactions, one is bounds of type variables and the other is variance. Let's look at type bounds first. Consider a method, assertAllPos, which takes an IntSet, returns that same IntSet if all its elements are positive and throws an exception otherwise. What would be the best type you can give to assertAllPos? Maybe this one, assertAllPos, it takes an IntSet and returns an IntSet. In most situations, this would be fine. But can one be more precise? But one observation is that assertAllPos would take Empty sets to Empty sets and NonEmpty sets to NonEmpty sets. If you want to express that in the type, then a way to do so is like this. We can say assertAllPos takes a type parameter S, which must be a sub-type of IntSet, and a value r of type S and it returns set type S. Here, the less than colon inset is an upper bound of the type parameter S. It means that S can be instantiated only to types that conform to IntSet. Let's see what one can do with assertAllPos. Can one pass an Empty set to it? Yes, because we can instantiate S to Empty. Then the actual set is an Empty set and we get back an Empty set. Can we pass a NonEmpty set to it? Yes, for the same reason. S can be instantiated to a NonEmpty set, which is a sub-type of IntSet, and we can pass a NonEmpty set and get back a NonEmpty set. How is this new version assertAllPos better than the old one? Well, in the return type, because now the return type is coupled with the parameter types. It expresses that whatever we put in, we get out. If you put in a specific subtype of IntSet, we get out that same IntSet. That was lost in the previous version where the return type was always IntSet, so the information what kind of IntSet it was, was lost. Generally, the notation S less than colon T means S is a sub-type of T, and S greater colon T would then mean S is a supertype of T or T is a sub-type of S. In fact, the supertype notation can also used as a bound and then it forms a lower bound, so you can write a lower bound for a type variable like this. That would introduce a type parameter S that can range only over supertypes of NonEmpty. Specifically, S could be NonEmpty or IntSet, which is a supertype or AnyRef or Any. We will see in the next session examples where lower bounds can be very useful. Finally, and for completeness, it's also possible to mix a lower bound with an upper bound. In that case, the lower bound comes first. To write an S which is lower bounded by NonEmpty and upper bounded by IntSet, you would write S greater colon NonEmpty, less colon IntSet. That means S can now be either IntSet or NonEmpty, but no other type. So much for bounds. Let's look at variance next. In fact, there's another interaction between subtyping and type parameters we need to consider. Given that NonEmpty is a sub-type of IntSet, do we also have that list of NonEmpty is a sub-type of list of IntSet? Intuitively, this makes sense. A list of NonEmpty sets is a special case of a list of arbitrary sets. We call types for which this relationship holds covariant because their subtyping relationship varies with the type parameters. If the type parameter goes up, then the type as a whole also goes up. They vary in the same direction and that's why they're called covariant. One question that's fair to ask is whether covariance makes sense for all types and not just for list. For perspective, let's look at arrays in Java and C sharp where they're similar. As a reminder, an array of elements of type T is written T brackets in Java, and in Scala, we use instead the parameterized type syntax array of T to refer to the same type. In fact, arrays in Java are covariant. One would have in Java that NonEmpty brackets as a sub-type of IntSet brackets. But, in fact, that covariance rule for arrays causes a lot of problems. To see why, consider the Java code below. We create a NonEmpty array which has a single element, which is some NonEmpty set. We then assign that array to an array of IntSets, then we create an Empty array, and put that into the first element of the IntSet array B. Finally, we pull out the first element of the A array and put that in a NonEmpty set S. Now what we need to know is when we have an assignment of arrays in Java, like this one here, then those two arrays really point to the same object. After the assignment, it would look like this. We would have the A array, which is an array which contains a single NonEmpty element, and the B array, which is an IntSet array, points to the same array. By the third line, we don't have a NonEmpty element in the array anymore. The element here is now Empty. That means by the fourth line, it seems that we assign an Empty element into a variable of type NonEmpty, which is of course a violation of type soundness. That means our type system lets us do something at runtime, which is clearly wrong. In fact, the Java runtime system patches the security hole by storing a runtime tag in arrays that remembers with what type it was created. The NonEmpty array here would remember that only NonEmpty elements can be stored. That means by the third line, this one here, you would actually get a runtime error similar to a class cast exception. It says, well, you can't store an Empty element in an array that was created by NonEmpty. But it's still troubling because we write something which looks completely reasonable and the end result is a runtime error, which is an array store exception similar to a class cast exception. Those things normally shouldn't happen. Now we're confused. Arrays can be covariant in Java. But when we use that, then we get into trouble, runtime exceptions. When is it sound to state that a type should be a subtype of another type? In fact, we can base reasoning on a principle that was first stated by Barbara Liskov, which tells us when a type can be a subtype of another. The principle says, "If A is a subtype of B, then everything one can do with a value of type B, the supertype, one should also be able to do with a value of type A." It means everything I can do with B, I can also do with a sub-type A, and potentially, I can do more things with A. In a sense, A is better than B. It can fulfill every role that B plays plus potentially more. The actual definition Liskov used is a bit more formal. It says, "Let q of x be a property provable about object x of type B. Then q of y should be provable for objects y of type A, where A is a subtype of B." These characterizations together have the name Liskov substitution principle. Now consider arrays again. An array of NonEmpty B, a subtype of an array of IntSet. That would mean that everything I can do with an array of IntSet, I can also do with an array of NonEmpty. But if you think about it, there is one thing I can do with an array of IntSet that I cannot do with an array NonEmpty, and that's store an Empty set in it. I can store an Empty set in an array of IntSet, but it's definitely not in an array of NonEmpty. By that logic, arrays cannot be covariant because covariance of arrays would violate the Liskov substitution principle. Let's look at the example again in Scala. What do you think? If you see the same four statements that we've seen before in Java but now written in Scala, would you observe a type error? If yes, in what line? Or would the program compile and maybe throw an exception at runtime, like it does in Java? Or would the program compile and run without exception? In fact, I've already given it away, since I said arrays cannot be covariant in Scala, that means that that assignment here that assigns an array of NonEmpty to an array of IntSet, that's illegal. Because an array of NonEmpty is not a subtype of an array of IntSet, so we would get a type error in line 2.