Generics in Scala: Upper and Lower Bound
“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