Scala Generics: Generalized Type Constraints (Part 3)
This entry in this series on Scala generics builds on type bounds and use site variance to tackle generalized type constraints.
Join the DZone community and get the full member experience.
Join For FreeThis is the third article in our series on Scala Generics (we have already looked at upper and lower Scala type bounds and covariance and contravariance) and today, we are going to talk about constraints — to be more specific, we're going to talk about generalized type constraints.
In Generics I, we talked about type bounds and use site variance. We also talked about control over abstract types, but 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 like this?
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
}
}
In these cases, we want to ensure that A is a Motorcycle type for parkMoto and Car is a type for parkCar, right?
If we remember Generics I, 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, we have 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, 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, 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… we use the methods parkMoto and parkCar 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 the 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 =:=, 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 cannot 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 curious use case could be the next one: I want a method for parking vehicles of the same class:
class Parking[A <: Vehicle](val vehicle: A) {
def park2(vehicle1: A, vehicle2: A) {}
}
As you have already deduced, this is not enough. The two vehicles could be any subtype of Vehicle, and if the parking we are creating is a 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 a subtype of A!
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 the 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 not have used =:=, but rather used <:< :
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 parameters, we can force one to 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 deprecated and is not in use in the Scala stdlib. It refers to the concept of “view”, also deprecated. That means that in A <%< B, A must be able to be seen as B. This can be given by an implicit conversion, for example.
These three Scala Generic articles are just an introduction to generics. We looked at some complex pieces, and while I would like say that we have reached some deep levels, we have actually 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 near future!
Published at DZone with permission of Rafael Ruiz Giner. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments