Lets Know About Generics And Variance In Scala

Reading Time: 5 minutes

Generics classes are classes which take a type as a parameter. This means, that one class can be used with different types without actually writing down it multiple times. These are particularly useful for collection classes.

1. Overview

In this blog, we’ll look at the advantages of Scala generics in implementing containers. We’ll examine how Scala geneics provide type-safety while helping us to stick to the DRY principle.

We’ll go through the steps of writing generics classes and methods and explore the generic types available in the standard Scala library.

2. Generics Method:

When writing Scala methods that take generics input or produce generic return values, the methods themselves may or may not be generic.

2.1. Generics Declaration Syntax:

Declaring a generics method is very similar to declaring a generics class. We still put type parameters in square brackets.

And, the return type of the method can also be parameterized:

def middle[A](input: Seq[A]): A = input(input.size / 2)

This method takes a Seq containing items of a chosen type and returns the item from the middle of the Seq:

val rabbits = List[Rabbit](Rabbit(2), Rabbit(3), Rabbit(7)) 
val middleRabbit: Rabbit = middle[Rabbit](rabbits)

Let’s now take a look at an example with more than one type parameter:

def itemsAt[A, B](index: Int, seq1: Seq[A], seq2: Seq[B]): (A, B) = (seq1(index), seq2(index))

This method takes the elements at index of both Seqs, and returns a tuple, matching the types used in the inputs:

val apples = List[Apple](Apple("gala"), Apple("pink lady"))
val items: (Rabbit, Apple) = itemsAt[Rabbit, Apple](1, rabbits, apples)

We should note that the above examples are not production-ready, as they do not correctly handle edge cases such as empty Seq. However, they show how type parameters can help us enforce the types of arguments and of return values.

2.2. Using Generics Classes in Non-Generics Method:

We don’t always write generic methods when using generic classes. For example, a method that takes two Lists of any type and returns the total length can be declared as:

def totalSize(list1: List[_], list2: List[_]): Int

The _ (underscore) means it doesn’t matter what’s in the List. There’s no type parameter, and so, there’s no need for the caller to provide the type for this method:

val rabbits = List[Rabbit](Rabbit(2), Rabbit(3), Rabbit(7))
val strings = List("a", "b")
val size: Int = totalSize(rabbits, strings)

In this example, the type is not supplied to the function when it is called.

3. Upper Type Bounds

To illustrate Scala’s upper type bounds, let’s reinvent the wheel by writing an elementary and yet generics function to find the maximum element in a collection. Here’s our first attempt:

def findMax[T](xs: List[T]): Option[T] = xs.reduceOption((x1, x2) => if (x1 >= x2) x1 else x2)

Even though the logic seems to be ok, the >=  function is not defined on the generic type T. Therefore, the findMax function won’t compile at all.

We know that the Ordered[T] is the home for such comparison functions. So, we should somehow tell the compiler that T is a subtype of Ordered[T] in this example.

As it turns out, the upper bound type in Scala generics will do this for us:

def findMax[T <: Ordered[T]](xs: List[T]): Option[T] = xs.reduceOption((x1, x2) => if (x1 >= x2) x1 else x2)

With the “T <: Ordered[T]” syntax we indicate that Ordered[T] is the supertype of type parameter TThat is, each element in the xs list should be a subtype of Ordered[T]. This way, we can use >= and other comparison functions with them.

4. Lower Type Bounds

Let’s take an example:

class Queue[+T](private val leading: List[T], trailing: List[T]) {
  def head(): T = // returns the first element
  def tail(): List[T] = // everything but the first element
  def enqueue(x: T): Queue[T] = // appending to the end
}

Here, we’re trying to represent a queue with two lists.

Now, suppose we need to work with a Queue[String]. Since we want Queue[String] to be a subtype of Queue[Any], we used the covariant type annotation [+T]. However, now the enqueue method won’t compile:

covariant type T occurs in contravariant position in type T of value x

def enqueue(x: T): Queue[T] = new Queue(leading, x :: trailing)

The type parameter [+T] is covariant, but we’ve used it in a contravariant position (function argument), and the compiler complains about that.

One way to fix this issue is to use lower bound types:

def enqueue[U >: T](x: U): Queue[U] = new Queue(leading, x :: trailing)

Here, we’re defining a new type parameter U. Also, the “U >: T” syntax means that U should be a supertype of T. Now, we can use the enqueue method:

val empty = new Queue[String](Nil, Nil)
val stringQ: Queue[String] = empty.enqueue("The answer")
val intQ: Queue[Any] = stringQ.enqueue(42)

When we add an Int to a Queue[String], the compiler automatically infers the nearest supertype for String and Int. Hence the returned type is Queue[Any].

5. Variance In Generics:

Variance defines Inheritance relationships of Parameterized Types. It is all about Sub-Typing. It makes Scala collections more Type-Safe.
Types of Variance:
1. Covariant
2. Invariant
3. Contravariant

class Stack[+A] // A covariant class
class Stack[-A] // A contravariant class
class Stack[A] // An invariant class

5.1 Covariant

If S is a subtype of T, then List[S] is a subtype of List[T].

In Scala, Syntax to represent covariant relationship between two parametrized types is prefixing Type Parameter with “+” symbol. Let’s understand covariance with the help of an example:

abstract class Flower 
{
    def name: String
}
  
// Creating a sub-class Lily 
// of Flower 
case class Lily(name: String) extends Flower
  
// Creating a sub-class Carnation
// of Flower 
case class Carnation(name: String) extends Flower 
object Covariance extends App
{
      
    // Creating a method
    def FlowerNames(flowers: List[Flower]): Unit =
    {   
        flowers.foreach 
        {
            flower => println(flower.name)
        }
    }
      
    // Assigning names
    val lily: List[Lily] = List(Lily("White Lily"), 
                                Lily("Jersey Lily"))
    val carnations: List[Carnation] = List(Carnation("White carnations"),
                                           Carnation("Pink carnations"))
  
    // Print: names of lily 
    FlowerNames(lily)
  
    // Print: names of carnation 
    FlowerNames(carnations)
}

5.2 Contravarient

If S is a subtype of T, then List[T] is a subtype of List[S].

In Scala, Syntax to represent contravariant relationship between two parametrized types is prefixing Type Parameter with “-” symbol. Let’s understand contravariance with the help of an example:
abstract class Type[-T]{
  def typeName(): Unit
}

class SuperType extends Type[AnyVal]{
  override def typeName(): Unit = {
    print("\n\n SuperType \n\n")
  }
}

class SubType extends Type[Int]{
  override def typeName(): Unit = {
    print("\n\n SubType \n\n")
  }
}

class TypeCarer{
  def display(t: Type[Int]){
    t.typeName()
  }
}

object ScalaContravariance {

  def main(args: Array[String]) {
    val superType = new SuperType
    val subType = new SubType

    val typeCarer = new TypeCarer

    typeCarer.display(subType)
    typeCarer.display(superType)
  }

}

5.3 Invariant

If S is a subtype of T then List[S] and List[T] don’t have Inheritance Relationship or Sub-Typing. That means both are unrelated.
In Scala, by default Generics Types have Non-Variant relationship. If we define Parameterized Types without using “+” or “” symbols, then they are known as Invariants. Let’s understand contravariance with the help of an example:

abstract class Animal {
  def name: String
}

case class Cat(name: String) extends Animal

case class Dog(name: String) extends Animal

class Container[A](value: A) {
  private var _value: A = value
  def getValue: A = _value
  def setValue(value: A): Unit = {
    _value = value
  }
}

object Invariance {

  val catContainer: Container[Cat] = new Container[Cat](Cat("tom"))

  // This won't compile
  val animalContainer: Container[Animal] = catContainer

}

6. Conclusion

To sum up this blog, we concluded the following:

  • Generic sclasses are classes which take a type as a parameter i.e., one class can be used with different types without actually writing down it multiple times.
  • Genrics Classes Methods
  • Variance defines Inheritance relationships of Parameterized Types.
  • Various types of variance are: Invariant, Covariant and Contravariant.
  • Invariant: If S is a subtype of T then List[S] and List[T] don’t have Inheritance Relationship or Sub-Typing.
  • Covariant: If S is a subtype of T, then List[S] is a subtype of List[T].
  • Contravariant: If S is a subtype of T, then List[T] is a subtype of List[S].

7. References

knoldus

Written by 

My name is Harshal Dubey. I am working as a Software Intern in Scala Studio at Knoldus. My hobbies are playing volleyball, football and Travelling.