Generics in Scala: Upper and Lower Bound

A black and white image of a laptop and a notebook faded and on the middle of the image there is the Scala logo in red for the post 5

“This post is also known as “My saga learning Scala - Part 5”

Part 1 - Why am I learning Scala and resources

Part 2 - Functions and Strings

Part 3 - Classes in Scala: method and attribute definition

Part 4 - Classes, Objects and Traits in Scala

As I said on the last post, Generics was a bummer and I got super demotivated. But after finding myself a mentor (thanks, Aaron Levin <3) and a bit more of study I figured out one of the complicated parts of this concept: the upper and lower bounds. Hope this makes it easier for you than it was for me 😉


Brief overview on Generics

Whenever I create a new class, I need to define the type of its parameters, so you are bounded to a specific type.

Take a look on the following class. Its unique function can only receive an Integer, regardless if the function can receive a String just as well. This is possible because the println will go to the method .toString that is implemented in the Integer class. But because .toString works in any class… this function could actually work with any class.

class NonGenericClass {
    def myFunction(param: Integer): Unit = {
      println(s"This was the param: ${param}")
    }
}

val instance = new NonGenericClass
instance.myFunction(1)
// This was the param: 1

Instead of being bounded to a single type we can make the class Generic by adding a [] with any value (such as T) to represent that this generic class will only define a type when it is instantiated:

class GenericClass[T] {
    def myFunction(param: T): Unit = {
      println(s"The param was: ${param}")
    }
  }

What this means is that this random letter will later be replaced by a class that will define and bound the type to the class. So, for instance, we can say that we want the same class to work with an integer:

val aIntegerClass = new GenericClass[Integer]
aIntegerClass.myFunction(2)
// The param was: 2

Or a String:

val aStringClass = new GenericClass[String]
aStringClass.myFunction("A String")
// The param was: A String

Or a Double:

val aDoubleClass = new GenericClass[Double]
aDoubleClass.myFunction(1.23)
// The param was: 1.23

You get the idea! In the case of our class, as long as the class has a .toString method implemented, it will work. Let’s override the method in a new class, so we can see the effect:

class AnyClass {
  override def toString = "any class can override this method!"
}

val overridingToString = new GenericClass[AnyClass]
overridingToString.myFunction(new AnyClass)
// The param was: any class can override this method!

The code above works well because all classes in Scala have a method .toString. What would happen if they didn’t?

Upper Bound: your safe guard on generics

Let’s say I want to create a class called Order that will create a new order for each person on a restaurant. The Order class will have a method, called getMeal, that checks if someone is vegetarian or not. Depending on the preferences it will return a specific meal.

To do this we will accept any class as a person in a new Order request. The restriction we have here is that the Person class have to have a method called eatsMeat.

You can imagine something like this:

// doesn't work
class Order[T](val person: T) {
  def getMeal: String = if (person.eatsMeat) "meet burguer" else "veg burguer"
}

The problem is that T can be any class. At this point we can’t guarantee that T will have a method eatsMeat. That is why the above code won’t work. If you try to compile it you will get the following error:

Error: value eatsMeat is not a member of type parameter T

So we need to make sure that T will always have the method eatsMeat.


We know that we have an abstract class Human, and Human is an abstract class that requires their subclasses to implement the .eatsMeat method:

abstract class Human {
  def eatsMeat: Boolean
}

So we know that any class that extends Human is good enough to be used as the T class we defined before.

The way we declare it is like this:

T <: Human // T is a subtype of Human

By doing that, our code will work but it will only accept classes that extends Human, thus, we guaranteed that they will always have the eatsMeat method.

// works
class Order[T <: Human](val person: T) {
  def getMeal: String = if (person.eatsMeat) "meet burguer" else "veg burguer"
}

Let’s imagine we have two classes and that both that inherits from Human:

class Adult(val vegetarian: Boolean) extends Human {
  override def eatsMeat: Boolean = !vegetarian
}
class Child(val vegetarian: Boolean) extends Human {
  override def eatsMeat: Boolean = !vegetarian
}

val jimmy = new Adult(true)
val marie = new Child(false)

We can now get meal for a vegetarian Jimmy and non vegetarian Marie:

val jimmyOrder = new Order(jimmy)
println(jimmyOrder.getMeal)
// "veg burguer"

val marieOrder = new Order(marie)
println(marieOrder.getMeal)
// "beef burguer"

This declaration T <: Human is called a Upper Bound which means that T is a subtype of Human.

Lower Bound: well…. they exist

If we needed the opposite, we can do T >: Human . This define that Human is a subtype of T.

In our restaurant we know that vegans should only eat vegan food while vegetarians will be happy eating both vegan or vegetarian food. We can also assume that ominivores will eat all types of food. That could be expressed like this:

class Vegan
class Vegetarian extends Vegan
class Ominivore extends Vegetarian

Ok, great! So If I am a vegetarian, I want my order to include either vegan or vegetarian but not meat. We could do that by defined a lower bound type:

class Order[T >: Vegetarian](meal: T)

This will allow this:

val firstOrder = new Order[Vegan](new Vegan)
val secondOrder = new Order[Vegetarian](new Vegetarian)

But not this:

val failOrder = new Order[Ominivore](new Ominivore) // doesn't work

I know the example explains but doesn’t show a real example. I couldn’t find a real use case in the examples I’ve seen. This post about Bound Types says the following:

  • The practical use cases for a Lower Type Bound are few and far between, but there certainly are some cases where it makes sense — just don’t expect to see them in every Scala codebase.

So I decided not to consume myself more with this and hopefully it will all make more sense whenever I see this concepts in “real life”


Ok! Hope you liked this post! Let me know if you are reading this and want a Part 6!


Cheers!
Letícia

Comments