So far we have seen how to define covariant and contravariant subtypes in Scala. In this post we will study lower bounds and upper bounds and see how they can be of great help when designing your application.
Consider the class I defined in the first post:
Company is covariant in type T as you know by now. It’s been a while and we realize it’s the time to define a partnership relation for companies to connect them together and expand their co-operations. So we add the following method to Company class:
When we try to compile this code we get:
Error: covariant type T occurs in contravariant position in type T of value y
What happened here? To see what has happened it’s better to take a look at Function1 definition in Scala:
This definition tells us that the input of a function is contravariant (negative) and what it returns is covariant (positive). So remember that no matter how many arguments a function has, all of them are in negative positions:
Now it should be more clear why we got the compile error. We were passing an argument of type T which is covariant (positive) and we know that arguments of a function has negative (contravariant) positions. This is a contradiction.
But how can we solve this? We can use lower type bounds to parameterize a method.
Lower type bounds: help you to declare a type to be a supertype of another type. T >: A indicates that type T or abstract type T refers to a supertype of type A.
Let’s correct the defined method by using lower bounds:
By this new definition, partnerWith method accepts arguments that can be supertype of type T.
scala> val littleCompany: Company[SmallCompany] = new Company[SmallCompany](new SmallCompany) littleCompany: Company[SmallCompany] = Company@149bc5a scala> val bigCompany: Company[BigCompany] = new Company[BigCompany](new BigCompany) bigCompany: Company[BigCompany] = Company@116ac04 scala> bigCompany.partnerWith(littleCompany)
So partnerWith method is now generalized and polymorphic.
Assume that we want to restrict the partnership relation somehow in our example. We wanna say that a company can have partnership with another company that is a subtype of Company[BigCompany]. In order to apply this restriction in Scala, one can use upper bounds. So let’s change the partnerWith method to reflect this:
Now partnerWith only accepts arguments that are a subtype of Company[BigCompany]. Let’s prove this by introducing a new company:
This class does not extend BigCompany so Company[CrappyCompany] is not a subtype of Company[BigCompany] according to covariant type declaration. Now we try to build partnerships in REPL:
scala> val littleCompany: Company[SmallCompany] = new Company[SmallCompany](new SmallCompany) littleCompany: Company[SmallCompany] = Company@1979eb4 scala> val bigCompany: Company[BigCompany] = new Company[BigCompany](new BigCompany) bigCompany: Company[BigCompany] = Company@6d1901 scala> littleCompany.partnerWith(bigCompany) scala> val crappyCompany: Company[CrappyCompany] = new Company[CrappyCompany](new CrappyCompany) crappyCompany: Company[CrappyCompany] = Company@1d0d313 scala> littleCompany.partnerWith(crappyCompany) <console>:11: error: inferred type arguments [Company[CrappyCompany]] do not conform to method partnerWith's type parameter bounds [U <: Company[BigCompany]] littleCompany.partnerWith(crappyCompany) ^
So by upper type bound declaration we could restrict our partnership relation.
Remember that by definition a class is supertype and subtype of itself. Hence you can pass an instance of Company[BigCompany] to partnerWith:
scala> val bigCompany2 : Company[BigCompany] = new Company[BigCompany](new BigCompany) bigCompany2: Company[BigCompany] = Company@b61e92 scala> bigCompany.partnerWith(bigCompany2)
Note that both lower and upper bound declarations have the colon as their second character so you don’t mix the order ( <: >: )
In these three posts I showed how you can avail from Scala variance annotations and lower and upper bounds together to design your application in a type safe manner. Scala provides you with type-driven design where types of an interface guides its detailed design and implementation . Examples provided in these posts were not the best examples but they could still show the purpose of their existence. I hope you have a better understanding of this cool feature in Scala.
: Martin Odersky, Lex Spoon, Bill Venners, “Programming in Scala”, 2nd Edition