Chapter 9: Type Constraints
In scala, with type parameters like [T] we pass any type, but sometimes we require to pass specified type of the data. In that case, type constraints come into the picture. Let’s take an below example:
According to the example, we are calling a saveTheDay
method with passing superhero object for saving the day of the city. But what if we are passing an object of supervillain for saving the day? Is it make sense? Let’s try:
In the above code, we are passing the object of Loki
to save the day of New York city. Is it digestible to you? This is our first problem, there is no way to control the object of any type for calling the method saveTheDay
because as we know, in JVM based languages type parametrization is used by the compiler and [T]
meany any type, and after type erasure it deletes type information and replace with Object
type, so we can pass any object to the method.
Note: This is never ever possible, Loki
is going to the save the day of the city, but he can destroy the city without any doubt.
Let’s jump into the second problem, as we know, in Avengers
all of them have one goal which is to stop the Thanos. So for that, we are adding one common behavior to Avengers
trait which is called stopTheThanos
as below:
As per the above code, we have a method called endGame
which is parameterized type, as we know, we can pass any type to this method but for saving the day from Thanos we need to call method stopTheThanos
, but the problem is [T] is replaced by Object
type as we discussed in chapter 7 after type erasure, and stopTheThanos
method is part of Avengers
type, so, how can we call this method?
Above two problems are the most common scenarios which we are facing while developing something with the help of parameterizing types. For solving that problem, Scala type system provides us beautiful feature which is called Type Constraints. Type constraints separated into two categories as below:
Least Upper Bound
Greatest Lower Bound
The general idea of type constraints is to add restrictions in our parameterize type for the specific type, which helps to restrict the scope of passed parameter and we can access specific behaviors or features of a restricted type.
Least Upper Bound ( <: )
The first problem, which we faced in our above code, we are going to resolve with the help of Least Upper Bound type constraints.
We have method saveTheDay
, and the problem with this method is, we can pass any type here. As we saw in our early example, we are able to pass Loki as a superhero to saving the city, which is completely wrong. So, with the help of Least Upper Bound, we are going to restrict our types within the passed method type argument as below:
In the above code, we are adding a special operator <: which is called Least Upper Bound or in the methematical term you can say that. < (less than) or : (equals to) to specific type. In our example, we have type hierarchy start from Avengers
which is supertype of other types called, IronMan
, Hulk
and more. With the help of Least Upper Bound, we restricted only those types which are same or subtype of Avengers
are allowed to pass as a superhero method parameters otherwise we are getting compile time error as below:
With the help of <: (Least Upper Bound), we successfully restrict our types and take advantage of compile-time safety for passing only defined types to this method. If we don’t have any type constraints than there is a possibility we will face this issues during runtime, which are really costly for resolve.
Now, jump to the second problem, where we want to use specific behavior of passed parameterize type within our method as below:
All Avengers have only one goal in end game which is to stop the Thanos and we know the endGame
method is parameterized type, but we want to call a stopTheThanos
method within the endGame
method from passed avengers type object. So, for solving that problem, again <: (Least Upper Bound) comes to the picture as below:
How this possible? why is it work? because we know, type erasure removes all type information and replace [T] with the Object
type.
But here, things are a bit different. In the above code, we added type constraints with parameterized type [T]. Which means, while compiler read this code and before converting it into bytecode, it checks constraints are present, that means, user can pass type which equals to or less than Avengers
type only, that means, rather than placing Object
instead of T, compiler will add Avengers
type instead of T as a method parameter and because of that, we can access all methods and features of Avengers
type from the passed object.
This is the concept of <: (Least Upper Bound) in Scala, and we have something similar in Java as well with the help of extends
keyword with parameterized type <T>.
Greatest Lower Bound ( >: )
In the mathematical term, >: (Greatest Lower bound) means type is > (greater than) or : (equals) to defined restricted type constraints as below:
With the help of >: (Greatest Lower bound) , we restricted our type parameter, that means to the wallOfSuperHerosTeam
method, we can pass only teams of super heros not individuals or we can say that we can pass Avengers
and its superclass, not its subclasses as below:
In this example, we can pass Avengers
and its super types only but not its subtype. In Java we have something similar to super
keyword, with generics type <T>.
Let's take some more complex and confusing example for the lower bound, but before proceeding further remember one thing:
In most of the static-based languages like Java, Scala, Haskell and in others, a type system is used for compile-time checks, so, user programs can reduce their mistakes during run-time. While designing and using generics or types, always think like a compiler or we can say “Be The Compiler”.
Let’s moving to our next examples:
There is no surprise in the above code compilation fails because Avengers
superclass is MavelHeroes
. DCHeroes
and MavelHeroes
are siblings. So, when compiler checks wallOfTeam[DcHeroes]
, and DcHeroes is not in the hierarchy, it fails compilation.
Now the question is, why this code compiles and run successfully? The answer is simple but a bit confusing. We are calling wallOfTeam
method with Heroes
type and as per type hierarchy Heroes
is a supertype of Avengers
, so, compiler checks Heroes
is valid type, and the method parameter object which we are passing is DcHeroes
, which is actually a subtype of Heroes
. According to JVM
based languages, we can pass subtype to its supertype in method arguments. So, according to that, the compiler allows for compile this code.
What happens now? While compiler compiles the above line of code, type inference comes into the picture. Type inference interprets this code and converts it into this :
Same as the last example code. This is the reason this compiles and run successfully. Under the hood >: (Greatest Lower bound) like [T >: Avengers]
it actually replaces [T] with Object/AnyRef
type, this is the reason it allows to pass any super type here. But with >: (Greatest Lower bound), we can only use to call those methods which are defined in Object/AnyRef
type. The compiler will not allow to call any specific method of a type. In Scala, >: (Greatest Lower bound) are mostly used with Variance for handling some specific scenarios, which we will discuss in our next chapter.
In the next chapter, we will see the how compiler thinks to solve variance, which always confused to scala developers.
Context-Bounds
In the last chapter, we design our type classes and perform ad-hoc polymorphism. For implementing type classes, we performed three steps and in the third step, we created a method, which is directly interacted to perform an operation on the basis of the passed abstract type instance as below:
In the above code, we need to define a method signature with implicit value. For designing Scala libraries, type classes are the most common pattern which is used by various libraries like Scalaz and Cats and you can see, with implicits, our method signature looks huge and boilerplate. So, for the ease perspective context-bound comes into the picture. With the help of context-bound we reduce our boilerplate code and makes our method compact like below:
As you can see, we defined [T: AnimalBehaviors]
, this doesn’t mean mathematical equals ( : ) like in our previous (<: , >: )bounds examples, here ( : ) means that the method defining an implicit parameter AnimalBehaviors
of type T as we defined in the first context bound example. With the help of implicitly[AnimalBehaviors]
, it will find the implicit object of AnimalBehaviors[T]
and call its behavior
method.
In the simplest way, you can think like that the method signature:
It converts it into below code by compiler
These are all bounds we have in Scala type system. If I am missing something, please let me know, so we can add here.
Last updated