Over a million developers have joined DZone.

Scala Generics (Part 1): Scala Type Bounds

DZone's Guide to

Scala Generics (Part 1): Scala Type Bounds

Dive into Scala generics with this primer on type bounds and Use-site variance as well as the pitfalls to avoid in your code.

· Java Zone ·
Free Resource

Java-based (JDBC) data connectivity to SaaS, NoSQL, and Big Data. Download Now.

Generic types, abstract types, scala type bounds: All these concepts are unusual for software developers who are coming from languages in which generics are not (or are 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 “car only” Parking lot?

new Parking[Car](new Jeep)

Jeep is subtype of Car, so 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 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, but 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 “Vehicle Parking”. 

Scala Type Bounds

Now that the introduction is done, let's bring on the interesting stuff.

In the previous example, there weren’t restrictions. We could create Parking[Vegetables] and the world would keep spinning. But 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 a 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 to a test and 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 these lines to 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 of ‘<:’.
[A >: Vehicle] will restrict A to supertypes of Vehicle, Vehicle included.

Its uses are mainly related with co- and contravariance. Those will be discussed in another article but let's quickly break down the lower type bound concept. 

Let's start by understanding which type of relationship represents a lower type bound. A >: B means that A must be B or a higher from B, B being 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 laying it out 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 can, so 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 now we get to the Big Question: What will happen with Tricycle?

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

Some of you probably weren’t expecting that, but it works. However, the compiler always has your back, and the type of Parking is now Parking[Bicycle].

Tricycle being a subtype of Bicycle, the example has searched for a supertype that matches the frontiers — and it has found it. Because of Liskov, 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 that 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 lets us 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 consists of 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 cannot do this:

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


It would be the same if we were using Thing, because AnyRef is a supertype of any type that we can define. 

If we don'y do Uuse-site variance, this would happen:

def dosomething(p1: Parking[Vehicle]) {}
var p1 = new Parking(new Car)
    <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)

It will only accept Parking[Vehicle].

The last thing I would like to mention is: Be aware of type inferencing.

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 raise the type of Car to Vehicle, creating a Parking[Vehicle]. That makes everything work again.

With these tools, you should be able to understand most code where generics are used and even start writing them yourself. 

In the following Scala articles, I will keep discussing generics, variance, and type constraints — as well as the reasons for using generics instead of polymorphism.  

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

Connect any Java based application to your SaaS data.  Over 100+ Java-based data source connectors.

java ,scala ,generics ,type bounds ,tutorial

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}