This is the third article in our series of Scala Generics ( we have already looked at upper and lower Scala Type bounds, Covariance and Contravariance) and today we are going to talk about constraints, to be more specific about generalized type constraints.
In Generics I we talk about the type bounds and use site variance, also we talked about the control over the abstract types, however there are methods that need to make sure that the abstract type of the class meets certain restrictions only in that method.
And today we are going to work with this small set of classes:
trait Thing
trait Vehicle extends Thing
class Car extends Vehicle
class Jeep extends Car
class Coupe extends Car
class Motorcycle extends Vehicle
class Vegetable
We will work with Parking, as usual.
class Parking[A <: Vehicle](val v: A){
def park: A = v
}
In this example, the parking method can return any type of vehicle, just as the upper type bound of Parking specifies, but what happens if we want to have specific logic to park cars and motorcycles, kind of this way?
class Parking[A <: Vehicle](val v: A) {
def parkMoto(): A = {
println("moto") // this could call some public method of Motorcycle
v
}
def parkCar(): A = {
println("car") // this could call some public method of Car
v
}
}
f
In these cases, we want to ensure that A is Motorcycle type for parkMoto and Car type for parkCar, right?
If we remember something from Generics I we will see that there were two ways to do similar things: type bounds on the method and with use site variance.
Let’s try it with the type bounds!
Let’s try to add bounds to A for the methods parkMoto and parkCar:
class Parking[A <: Vehicle](val v: A) {
def parkMoto[A <: Motorcycle] = {
println("moto") // this could call some public method of Motorcycle
v
}
def parkCar[A <: Car] = {
println("car") // this could call some public method of Car
v
}
}
If you put this in an IDE this will give you clues … Suspicious shadowing … But it does not matter, it compiles and we will try it!
val p1 = new Parking(new Motorcycle)
p1: Parking[Motorcycle] = Parking@193f604a
p1.parkCar
res5: Motorcycle = Motorcycle@5562c41e
p1.parkMoto
res6: Motorcycle = Motorcycle@5562c41e
It seems that those type bounds have not done anything. Obviously, the clue that the IDE gave us: Suspicious shadowing by a type parameter means that we are redefining the type parameter.
It turns out that we were not adding bounds to our A, but defining a new type A …
And if we type the return of the method to be sure?
class Parking[A <: Vehicle](val v: A) {
def parkMoto[B <: Motorcycle]: B = {
println("moto") // this could call some public method of Motorcycle
v
}
}
<console>:13: error: type mismatch;
found : Parking.this.v.type (with underlying type A)
required: B
v
^
It is not enough with that, since we are adding restrictions on the type of return .. but we want to work with v: A
In other words, the restrictions should not go on a new type B, but on the type A defined in the class, it seems that the type bound does not work for us …
Let’s try using use site variance, if we remember it, use site variance allowed us to define the constraints of a generic type at the moment of defining it:
class Parking[A](val v: A) {}
def parkMoto(parking: Parking[_ <: Motorcycle]) = {
println("moto") // this could call some public method of Motorcycle
parking.v
}
def parkCar(parking: Parking[_ <: Car]) = {
println("car") // this could call some public method of Car
parking.v
}
Looks good, let’s check it out:
parkCar(new Parking(new Car))
res1: Car = Car@17baae6e
parkCar(new Parking(new Motorcycle))
<console>:14: error: type mismatch;
found : Motorcycle
required: Car
parkCar(new Parking(new Motorcycle))
^
It seems that this can be a solution, although we have had to sacrifice several things … the methods parkMoto and parkCar we use them…outside Parking, passing a parking as a parameter .. In addition, the open-close principle has been broken by calling parking.v (tell do not ask).
Although it works, it is a very poor solution to our problem.
And here is where the Generalized type constraints come into play:
The three existing generalized type constraints are =: =, <: <and <% <. They are used by implicit parameters (implicit ev: T =: = B) in the method.
These implicit parameters, generally called ev (“evidences”) are tests, which show that a type meets certain restrictions.
These constraints can be used in different ways, but the most interesting thing is that they allow us to delimit the type parameter of the class in the same method:
class Parking[A <: Vehicle](val v: A) {
def parkMoto(implicit ev: A =:= Motorcycle) { println("moto") }
def parkCar(implicit ev: A =:= Car) { println("car")}
}
By using =:= we have achieved that an abstract type such as Parking, forces its type parameter to be a specific one for different methods.
And what will happen if I create a Parking [Car] and call it parkMoto?
val p1 = new Parking(new Car)
p1: Parking[Car] = Parking@5669f5b9
p1.parkCar
p1.parkMoto
<console>:14: error: Cannot prove that Car =:= Motorcycle.
p1.parkMoto
^
Indeed, we have managed to have methods that only work when the type parameter meets certain restrictions.
Mainly the generalized type constraints serve to ensure that a specific method has a concrete constraint, so that certain methods can be used with one type and other methods with another.
However, due to the type erasure, we can not overload a method:
class Parking[A <: Vehicle](val vehicle: A) {
def park(implicit ev: A =:= Motorcycle) { println("moto") }
def park(implicit ev: A =:= Car) { println("car") }
}
method park:(implicit ev: =:=[A,Car])Unit and
method park:(implicit ev: =:=[A,Motorcycle])Unit at line 12
have same type after erasure: (ev: =:=)Unit
def park(implicit ev: A =:= Car) {}
Another use curious use case could be the next one: I want a method for parking to vehicles ofthe same class:
class Parking[A <: Vehicle](val vehicle: A) {
def park2(vehicle1: A, vehicle2: A) {}
}
As you have already deduced this is not enough, since the two vehicles could be any subtype of Vehicle, and if the parking we are creating is new Parking (new Car), we could park one Jeep and one Coupe at a time.
The solution is a generalized type constraint:
class Parking[A <: Vehicle] {
def park2[B, C](vehicle1: B, vehicle2: C)(implicit ev: B =:= C) {}
}
Now let’s try it:
val p2 = new Parking[Car]
a: Parking[Car] = Parking@57a68215
p2.park2(new Jeep, new Jeep)
p2.park2(new Jeep, new Coupe)
<console>:15: error: Cannot prove that Jeep =:= Coupe.
a.park2(new Jeep, new Coupe)
^
Now vehicle1 must be the same type as vehicle2, however …
p2.park2(new Vegetable, new Vegetable)
Oops … we have lost the Vehicle constraint. Let’s fix it! Type bounds to the rescue!
class Parking[A <: Vehicle] {
def park2[B <: A, C](vehicle1: B, vehicle2: C)(implicit ev: B =:= C) {}
}
val p3 = new Parking[Car]
p3.park2(new Vegetable, new Vegetable)
<console>:14: error: inferred type arguments [Vegetable,Vegetable] do not conform to method park2's type parameter bounds [B <: Car,C]
p3.park2(new Vegetable, new Vegetable)
^
<console>:14: error: type mismatch;
found : Vegetable
required: B
p3.park2(new Vegetable, new Vegetable)
^
<console>:14: error: type mismatch;
found : Vegetable
required: C
p3.park2(new Vegetable, new Vegetable)
^
<console>:14: error: Cannot prove that B =:= C.
p3.park2(new Vegetable, new Vegetable)*/
p3.park2(new Jeep, new Coupe)
<console>:15: error: Cannot prove that Jeep =:= Coupe.
p3.park2(new Jeep, new Coupe)
^
p3.park2(new Jeep, new Jeep)
Now the Parking2 method can only receive two identical types that must also be A or subtype of A, fixed!
And finally, let’s look at other two generalized type constraints:
A <:< B, as you may guess, means "A must be a subtype of B. It is analogous to type bound <:
Its use is exactly the same as with =:=, with an implicit “evidence”.
In the previous case of parkCar and parkMoto, if we wanted to not only park cars and motorcycles but subtypes of cars and motorcycles as well, we would don use this =:=, but rather use this <:< :
class Parking[A <: Vehicle](val v: A) {
def parkMoto(implicit ev: A <:< Motorcycle) { println("moto") }
def parkCar(implicit ev: A <:< Car) { println("car")}
}
And of course, in the case of receiving two type parameter, we can force that one should be the subtype of the other:
class Parking[A <: Vehicle] {
def park2[B <: A, C](vehicle1: B, vehicle2: C)(implicit ev: B <:< C) {}
}
The last generalized type constraints is <%<, is deprecated and is not in use in the Scala stdlib. It refers to the concept of "view", also deprecated. Means that in A <%< B, A it must be able to be seen as B. This can be given by an implicit conversion, for example.
For those who want more information about generalized type constraints, to know how they work internally, and have more comparisons with type points etc., I strongly recommend to read this post.
These three Scala Generic articles are just an introduction to generics.We looked at some complex pieces and I should say that we have reached quite deep levels, however we have only scratched the surface. We have not even entered into what their implementations are !!
I hope that I will have time to write more articles about Scala in the nearest future!
If you found this article about covariance and contravariance in generics interesting, you might like…
Scala generics I: Scala type bounds
Scala generics II: covariance and contravariance
F-bound over a generic type in Scala
Microservices vs Monolithic architecture
The post Scala Generics III: Generalized type constraints appeared first on Apiumhub.