Back2Basics: Scala Type System in Depth

Table of contents
Reading Time: 5 minutes

In this blog, I will put emphasis on the power and awesomeness of the Scala Type System. Also, I will try to reiterate that it is not difficult or complicated as perceived.  In layman terms, the Scala Type system helps us keep the code tidy and type safe.  So in this blog, I shall take you through the following :

  • Parameterized types
  • In-Variance
  • Co-Variance
  • Contra-Variance

Pre-Requisite

Back2Basics: Introduction to Scala Type System

Type Parameterization

Type parameterization allows us to write generic classes and Traits. In Scala, we have to provide type parameters while defining generic classes and Traits.  Scala provides us with the leverage to do type checking by tightening or relaxing the constraints on the type parameters. There are two forms of these constraints mainly, that are broadly classified as Bounds and Variance.

Variance

Variance can be broadly subclassified into In-variance, Covariance and Contravariance. Let us understand each of it with an example: Invariance is the default behaviour in Scala. Consider that we have the following defined:

Screenshot from 2018-04-15 01-06-04

abstract class Chocolate {
  def name: String
}

class Ferrero extends Chocolate {
  def name = "Ferrero"
}

class Toblerone extends Chocolate {
  def name = "Toblerone"
}
Screenshot from 2018-04-15 01-06-16
abstract class Box {
  def chocolate: Chocolate
  def contains(aChocolate: Chocolate) = chocolate.name.equals(aChocolate.name)
}

class FerreroBox(ferrero:Ferrero) extends Box {
  def chocolate: Chocolate = ferrero
}

class TobleroneBox(toblerone: Toblerone) extends Box {
  def chocolate: Chocolate = toblerone
}

As per the code above, the definition of the two classes FerreroBox and TobleroneBox here gives us additional type safety, since the return type of the method chocolate is additionally restricted to Ferrero and Toblerone, respectively. Design of this class hierarchy quickly leads us to ask the question “whether the cost of maintaining the code is worth the gains on the side of type safety?”

In order to quickly avoid these kinds of trade-offs, Scala allows us to parameterize classes. That implies that Scala allows us to use type parameters instead of using a real type. These type parameters must be declared in the definition of a class and must be bound to a real type when instantiating that class. Similarly, the type parameter can be seen as a name for a type that is bound when instantiating the class.

Let’s take a step further and replace the concrete return type Chocolate of Box.chocolate with a type parameter T, and additionally restrict it a subtype of Chocolate itself (by adding T <: Chocolate). The modified class Box is then as follows :

class Box[T <: Chocolate](aChocolate: T) {
  def chocolate: T = aChocolate
  def contains(aChocolate: Chocolate) = chocolate.name == aChocolate.name
}

val ferreroBox: Box[Ferrero] = new Box[Ferrero](new Ferrero)

val tobleroneBox: Box[Toblerone] = new Box[Toblerone](new Toblerone)

By parameterizing Box, we implicitly defined at least two new types: Box[Ferrero] and Box[Toblerone]. Now if you observe, the types get related to one another with help of the variance annotations.

The point worth noting is that the classes Box[Ferrero] doesn’t inherit Box[Chocolate] in the above example – this is because there is the assumption that the Scala compiler makes when there is no variance annotation. Therefore, we can’t assign an object of type Box[Ferrero] to a Box[Chocolate]-typed variable:

val simpleBox:Box[Chocolate] = new Box[Ferrero](new Ferrero)

The above expression yields us with a compile-time error: Expression of type Box[Ferrero] doesn’t conform to expected type Box[Chocolate]. This is the default behaviour in Scala. This is called as Invariance. The relationship that Ferrero is a Chocolate doesn’t establish the relationship that Box of Ferrero is a Box Of Chocolate. In other words, the invariance says that if we have a relation that Ferrero extends Chocolates then compiler can’t infer the same relationship between a Box of Ferrero and Box of Chocolates.  

Screenshot from 2018-04-15 01-06-34

 Figure [1] : Invariance  

Variance annotations to type parameter declarations are added with a + (meaning covariance) or a – (meaning contravariance). The class header of Box can be modified to allow the above assignment:

class Box[+T <: Chocolate](aChocolate: T) {
  def chocolate: T = aChocolate
  def contains(aChocolate: Chocolate) = chocolate.name == aChocolate.name
}

The assignment of a Box[Ferrero] to a variable of type Box[Chocolate] is now possible since the covariance annotation +T made Box[Ferrero] a subclass of Box[Chocolate].

Now the expression below no longer gives a compilation error.

val simpleBox:Box[Chocolate] = new Box[Ferrero](new Ferrero)

Screenshot from 2018-04-15 01-06-46

Figure [2] Co-variant

Similarly, Contra-variance is just opposite of covariance. Let us understand it by extending the above example, let us add a new variant of Ferrero namely AlmondFerrero and a FlavourBox:

class AlmondFerrero extends Ferrero {
  override def name: String = "Almond Ferrero"
}

class FlavourBox[-T <: AlmondFerrero](aChocolate: T) {
  def chocolate: AlmondFerrero = aChocolate
  def contains(aChocolate: Chocolate) = chocolate.name == aChocolate.name
}

Contra-variance will allow me to establish the following relationship (the supertype parameter is allowed to be instantiated  to a lower class reference)

val almondFerrero:FlavourBox[AlmondFerrero]=new FlavourBox[Ferrero](new Ferrero)

Screenshot from 2018-04-15 01-06-58

Figure [3] Contra-variant

Parameterized types are invariant by default if no variance annotation is specified. A variance annotation creates a type hierarchy between parameterized types that are derived from the type hierarchy of the used types. The class diagrams Figure [1] and Figure [2] illustrate the inheritance relation between Box[Chocolate] and Box[Ferrero] when declaring T invariant, covariant and Figure [3] illustrates the inheritance relation between FlavourBox[Ferrero] and FlavourBox[AlmondFerrero] when declaring T as contravariant.

With covariance, the type hierarchy of the injected types is used, and with contravariance, their hierarchy is inverted. With invariance, the type hierarchy is completely ignored.

From the developer’s view, co- and contravariant type parameters can be visualised as tools to extend the type checking in generic classes. They provide additional type safety, which also implies that this feature offers new options for leveraging type hierarchies without the need to compromise upon on type safety.

Happy Reading!

knoldus-advt-sticker

Written by 

Pallavi is a Software Consultant, with more than 3 years of experience. She is very dedicated, hardworking and adaptive. She is Technology agnostic and knows languages like Scala and Java. Her areas of interests include microservices, Akka, Kafka, Play, Lagom, Graphql, Couchbase etc. Her hobbies include art & craft and photography.

3 thoughts on “Back2Basics: Scala Type System in Depth5 min read

Comments are closed.

Discover more from Knoldus Blogs

Subscribe now to keep reading and get access to the full archive.

Continue reading