3

Scala: Generic classes and Variance

 2 years ago
source link: https://blog.knoldus.com/scala-generic-classes-and-variance/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Reading Time: 4 minutes

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

Defining a generic class:

Generic classes take a type as a parameter within square brackets [ ]. One convention is to use the letter A as type parameter identifier, though any parameter name may be used. Let us understand this with the help of an example. As I have already described that generic classes are useful for collection classes, So let’s try to understand the Stack collection.

xxxxxxxxxx
class MyStack[A](elems: List[A]) {
  def isEmpty: Boolean = elems.isEmpty
  def push(elem: A): List[A] = {
    elem :: elems
  }
  def pop: List[A] = {
    if(!isEmpty) {
      elems.tail
    }
    else {
      throw new NoSuchElementException("pop of an empty stack")
    }
  }
}

This implementation of a MyStack class takes type A as a parameter. This simply means the parameter elems can only store elements of type A. The method push only accepts objects of type A.

Now as we understood how to define the generic classes, let’s understand how to use them.

Usage:

To use a generic class, put the type in the square brackets in place of A.

xxxxxxxxxx
val list = List(1, 2, 3, 4)
val stack = new MyStack[Int](list)
stack.push(5)
stack.push(6)
println(stack.pop)  // prints 6
println(stack.pop)  // prints 5

Here, the stack can only take Ints. However, if the type argument had subtypes, those could be passed in:

xxxxxxxxxx
class Animal
class Cat extends Animal
class Dog extends Animal
val list = List[Animal]()
val cat = new Cat
val dog = new Dog
val stack = new MyStack[Animal](list)
stack.push(cat)
stack.push(dog)

Here, class Cat and Dog both extends Animal so we can push instances cat and dog onto the stack of Animal.

Variance:

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

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

Covariant:

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

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:

xxxxxxxxxx
class Animal[+T](val animal:T)
class Dog
class Puppy extends Dog
class AnimalCarer(val dog: Animal[Dog])
object ScalaCovariance extends App {
  val puppy = new Puppy
  val dog = new Dog
  val puppyAnimal: Animal[Puppy] = new Animal[Puppy](puppy)
  val dogAnimal: Animal[Dog] = new Animal[Dog](dog)
  val dogCarer = new AnimalCarer(dogAnimal)
  val puppyCarer = new AnimalCarer(puppyAnimal)
}

As we can see above, class Puppy is a subtype of Dog and we are able to define the dogCarer and puppyCarer both but if we remove the “+” sign we get a compile-time error.

Contravariant:

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

scala-contravariance

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:

xxxxxxxxxx
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)
  }
}

As we can see above, class SuperType is a subtype of Type[AnyVal] and class SubType is a subtype of Type[Int]. We can see typeCarer object is able to display both SuperType and SubType but if we remove the “-” sign we get a compile-time error.

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 Generic 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:

xxxxxxxxxx
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
}

As we can see above, class Cat and Dog are subtypes of Animal. So catContainer compiles fine whearas animalContainer throws compile time error saying Container of type Animal cannot hold Container of type Cat.

Check out my Github repo for complete scala tutorial

Conclusion:

To sum up this blog, we concluded the following:

  • Generic classes 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.
  • 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].

References:

Knoldus-blog-footer-image


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK