Generic types in Scala

Generic types, abstract types, scala type bounds, all these concepts are unusual for software developers who are coming from languages in which the generics are not (or barely) used, so in this first article, we will discuss the basics and try to dig down only in type bounds.

Let´s work with this little set of types and we will continuously modify the Parking type.

trait Thing 
trait Vehicle extends Thing 
class Car extends Vehicle 
class Jeep extends Car 
class Coupe extends Car 
class Motorcycle extends Vehicle 
class Vegetable 

class Parking[A](val place: A) 

Nomenclature: If we think about Thing[A], Thing is a generic type, and (A) is the abstract type.

Before starting, we should understand how generic types work and the type inference in Scala.

Parking[A] indicates that the value we want to pass to “place” must be of type A, so, if we create Parking[Motorcycle], we will create it with an instance of Motorcycle:

 new Parking[Motorcycle](new Motorcycle)

If we try this:

 new Parking[Motorcycle](new Car)

This won’t compile, obviously, since Car and Motorcycle are not the same type.
So, what happens when we try to park a Jeep in a “only car” Parking lot?

 new Parking[Car](new Jeep)

Jeep is subtype of Car,  of course it works!

As I mentioned before, let´s discuss the inference of types.
First, let’s change Parking a little bit:

 class Parking[A](val place1: A, val place2: A)

If we follow the last example, Parking[Car] should receive two cars, two Jeeps, car and a Jeep, or something similar. But what happens if we don´t specify the type?

 new Parking(new Car, new Motorcycle)

It compiles and it is the compiler that assigns the type A. The type inference makes us the task easier but, what type is A now that we haven´t indicated it?

The compiler will evaluate the type of both parameters, if they were the same type, it would be easy, however since they are not, it searches for the nearest common supertype.
An easy way to check this is to create this Parking on Scala REPL. The print follows clearly:

 val a = new Parking(new Car, new Motorcycle)
 *a: Parking[Vehicle] = Parking@278e1674*

It understood that “Parking” must be a “Vehicle Parking”.

 

Scala type bounds


Once the introduction is done, let´s bring on the table the interesting stuff.

In the previous example there weren’t restrictions, we could create Parking[Vegetables] and the world would keep spinning, however in real life we want to impose certain rules.
How can we create type parameter restrictions? To create them we make use of scala type bounds.

 

> Upper type bounds

 class Parking[A <: Vehicle]

The easier type bound to understand is upper type bound ‘<:’, this indicator would be the same as ‘:’ when we create a value and we give it a specific type.
val a: Parking means that “a” must be an instance of Parking or a subtype of Parking. 
In the type scenario, Parking[A <: Vehicle] means that the A type must be type or subtype of Vehicle.

Because of that, if we create a Vehicle, Car, Jeep or Motorcycle Parking, everything works.

The following lines could be added in a test and it the result would be SUCCESS:

 new Parking[Vehicle] shouldBe a[Parking[_]]
 new Parking[Car] shouldBe a[Parking[_]]
 new Parking[Jeep] shouldBe a[Parking[_]]
 new Parking[Motorcycle] shouldBe a[Parking[_]]

But if we try to create a Vegetable or Thing Parking it won’t compile. Our compiler protects us:
If we add this lines in a test, it won’t even compile:

 new Parking[Vegetable] should be(a[Parking[_]])
 new Parking[Thing] should be(a[Parking[_]]) 
 * _ refers to existential types: https://typelevel.org/blog/2016/01/28/existential-inside.html

 

> Lower type bounds

On the other hand we have the lower type bound ‘>:’, which indicates the opposite to ‘<:’.
[A >: Vehicle] will restrict A to supertypes of Vehicle, Vehicle included.
Its uses are mainly related with co and contravariance even though this topic concerns to another article, we will break down the lower type bound concept:
Let´s start understanding which type of relationship represents a lower type bound. A >: B means that A must be B or higher from B, being B the frontier (bound).

 class Parking[A >: Jeep](val place: A) 

In this Parking we could park any supertype of Jeep, meaning, Car, Vehicle, Thing… AnyRef.. Well, it seems a little too generic, doesn´t it?

 class Parking[A >: Jeep <: Vehicle](val plaza: A)

Here is a way of lay it, if we imagine a type tree from that style:

 trait Thing
 class Vehicle extends Thing
 class Car extends Vehicle
 class Jeep extends Car
 class Coupe extends Car
 class Motorcycle extends Vehicle
 class Bicycle extends Vehicle
 class Tricycle extends Bicycle

Can we limit Parking to all the subtypes of Vehicles, above Tricycle?

 class Parking[A >: Bicycle <: Vehicle](val plaza: A)

Looks like we do can, let´s test it:

 new Parking(new AnyRef)
 <console>:12: error: inferred type arguments [Object] do not conform to class Parking's type parameter bounds [A >: Bicycle <: Vehicle]
  new Parking(new AnyRef)
  ^
  <console>:12: error: type mismatch;
  found : Object
  required: A
  new Parking(new AnyRef)
                   ^

AnyRef gives us a compilation error, perfect, we see that the upper type bound works.

 new Parking(new Bicycle)
 res5: Parking[Bicycle] = Parking@1f3f425b
 new Parking(new Vehicle)
 res6: Parking[Vehicle] = Parking@61fb801b
 new Parking(new Coupe)
 res7: Parking[Vehicle] = Parking@e959286

And we got to the Big Question, what will happen with Tricycle?

 new Parking(new Tricycle)
 res8: Parking[Bicycle] = Parking@53c20385

Probably some of you weren’t expecting that, but it works.
However, the compiler always has your back, and the type of Parking is now Parking[Bicycle].
Being Tricycle a subtype of Bicycle, the example has searched for a supertype which matches with the frontiers and it has found it. Because of Liskov there where you can use a Bicycle, you should be able to use a Tricycle as well.

However, this is not the most common use for lower type bounds,
They are used to put a covariant type in a contravariant position (in the next article, I will bring this concept up). Just so you have that clear, if you create a covariant Thing[+A], there are some rules that block you from using it in certain positions, such as:

 class Parking[+A] {
  def parkIt(element: A): Parking[A] = new Parking(element)
 }

Even though it looks correct, it is not: We are using A in a contravariant position even though it is a covariant type. Seriously, don’t worry if don´t understand what it means, it will be explained in another article, just focus on the prohibition from the compiler.
So, how can we have a method which allows us to create Parking depending on the parameter type?
Lower type bounds:

 class Parking[+A] {
  def parkIt[B >: A](element: B): Parking[B] = new Parking(element)
 }

Because the lower type bound includes the frontier, adding a lower type bound we could use the covariant A type to generate the limit of the lower type bound from B and let B be the one who types everything. Tada!!!

 

Use-site variance

Another use for scala type bounds is Use-site variance. It is the form of variance that is used in Java.
Use-site variance consist in setting the bounds when declaring the type, if our Parking were invariant, it would be the unique way of doing any type of variance:

 class Parking[A](val place: A){
  def dosomething(p1: Parking[_ <: Vehicle]) {}
 }

Even though our Parking doesn´t have Bounds nor covariance, it would accept any type being A, in our method dosomething we have created a limit, now we can do this:

 dosomething(new Parking(new Car))

But we can NOT do this:

 dosomething(new Parking(new AnyRef))
 <console>:12: error: type mismatch;
 found : Object
 required: Vehicle
 dosomething(new Parking(new AnyRef))
                               ^

 

It would happen the same if we were using Thing, because AnyRef is a supertype of anytype that we can define.
If we wouldn´t do use-site variance, this would happen:

 def dosomething(p1: Parking[Vehicle]) {}
 var p1 = new Parking(new Car)
 dosomething(p1)
 <console>:14: error: type mismatch;
 found : Parking[Car]
 required: Parking[Vehicle]
 Note: Car <: Vehicle, but class Parking is invariant in type A.
 You may wish to define A as +A instead. (SLS 4.5)
 dosomething(p1)
             ^ 

It will only accept Parking[Vehicle]

The last thing i would like to mention is: be aware with type inference
If we don’t explicit the types, strange things can happen. We just witnessed how dosomething does not accept p1 because it wasn’t the Parking type it was expecting, but what will happen if we do this:

 dosomething(new Parking(new Car))

The type inference will look for a matching type: it will rise the type of Car to Vehicle, creating a Parking[Vehicle]. Making everything work again.

With this tools you should we able to understand most of the codes where generics are used and even start writing them yourself.
In the following scala articles, I will keep discussing about generics, variance and type constraints. As well as the reasons of why using generics instead of polymorphism. 

 

If you are working on a Scala project and you need help with software architecture or development, let us know! We would be happy to know more! 

If you are interested in Scala type bounds or in software development best practices, I recommend you to subscribe to our monthly newsletter to receive latest tips. 

 

If you found this article about Scala Type Bounds interesting, you might like…

 

BDD: user interface testing

F-bound over a generic type in Scala

Microservices vs Monolithic architecture

“Almost-infinit” scalability