Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

The Scala Type System in Depth

DZone's Guide to

The Scala Type System in Depth

Let's take a look at the Scala type system, including dives into covariance, invariance, and contravariance, to see how type safety is maintained.

· Java Zone ·
Free Resource

Get the Edge with a Professional Java IDE. 30-day free trial.

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 as difficult or complicated as perceived. In plain terms, the Scala type system helps us keep code tidy and type safe. So in this blog, I shall take you through the following:

  • Parameterized types
  • In-variance
  • Co-variance
  • Contra-variance

Prerequisite

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 go over each of those with an example: invariance is the default behavior in Scala. Consider that we have the following defined:

abstract class Chocolate {
    def name: String
}

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

class Toblerone extends Chocolate {
    def name = "Toblerone"
}
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 of 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 to 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 from the variance annotations.

The point worth noting is that the class 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 the opposite of covariance. Let us understand it by extending the above example, adding 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 in Figure [1] and Figure [2] illustrate the inheritance relation between Box[Chocolate] and Box[Ferrero] when declaring T invariant and 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 visualized 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 type safety.

Get the Java IDE that understands code & makes developing enjoyable. Level up your code with IntelliJ IDEA. Download the free trial.

Topics:
java ,scala ,type safety ,invariance ,covariance ,contravariance ,tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}