Scala Tutorial - Learn How To Create Contra-Variance Type Class
Overview
In this tutorial, we will continue from what we've learned from the previous tutorials on Class Inheritance, Case Class Inheritance, Type Class and Covariance Type Parameter in Scala.
The examples in this tutorial will be a review of inheritance in Scala with classes and case classes which extend an abstract class. In addition, we will also define a class which expects a given type - type class.
To follow on from the previous tutorials on Type Class and Covariance Type Parameter, we will show example of enforcing type hierarchies through the use of variance - in particular contra-variance.
If you recall from the tutorial on What is Scala programming language, Scala is both an Object Oriented and Functional programming language. As a result of its Object Oriented nature, it has full support of object hierarchies through the use of class inheritance.
Steps
1. How to define an abstract class called Donut
When dealing with class and object hierarchies, you typically have a base class which encapsulates common behaviour. As an example, let's create an abstract class name Donut which defines a method signature for printName.
println("Step 1: How to define an abstract class called Donut")
abstract class Donut(name: String) {
def printName: Unit
}
NOTE:
- Any class which extends the abstract class Donut will have to provide an implementation for the printName method.
2. How to extend abstract class Donut and define a case class called VanillaDonut
To create a sub-class of the abstract Donut class from Step 1, you have to use the extends keyword, pass-through the name constructor parameter and also provide an implementation for the printName method.
As shown in the example below, the case class VanillaDonut extends the abstract class Donut and also provides an implementation for the printName method.
println("\nStep 2: How to extend abstract class Donut and define a case class called VanillaDonut")
case class VanillaDonut(name: String) extends Donut(name) {
override def printName: Unit = println(name)
}
NOTE:
- Unlike the previous tutorial on Class Inheritance, we did not have to create a Companion Object for the VanillaDonut case class because the case class will automatically provide a companion object.
3. How to extend abstract class Donut and define another case class of Donut called GlazedDonut
Similar to Step 2, let's create another case class named GlazedDonut which extends the abstract Donut class.
println("\nStep 3: How to extend abstract class Donut and define another case class called GlazedDonut")
case class GlazedDonut(name: String) extends Donut(name) {
override def printName: Unit = println(name)
}
NOTE:
- Similar to Step 2, we did not have to create a Companion Object for the GlazedDonut class.
4. How to instantiate Donut objects
Let's create two Donut objects, one using the VanillaDonut class and the other one using the GlazedDonut class. Note that we are specifying the type for both vanillaDonut and glazedDonut to be of base type Donut.
println("\nStep 4: How to instantiate Donut objects")
val vanillaDonut: Donut = VanillaDonut("Vanilla Donut")
vanillaDonut.printName
val glazedDonut: Donut = GlazedDonut("Glazed Donut")
glazedDonut.printName
You should see the following output when you run your Scala application in IntelliJ:
Step 4: How to instantiate Donut objects
Vanilla Donut
Glazed Donut
NOTE:
- Since VanillaDonut and GlazedDonut are sub-classes of the base class Donut, they both have access to the printName method.
5. How to define a ShoppingCart type class which expects Donut types
The above steps should be familiar already if you have followed the previous tutorials on Class Inheritance and Case Class Inheritance.
What if you had to define a class which expects a particular type parameter? As an example, let us define a ShoppingCart class which expects a sequence of Donut types.
println("\nStep 5: How to define a ShoppingCart type class which expects Donut types")
class ShoppingCart[D <: Donut](donuts: Seq[D]) {
def printCartItems: Unit = donuts.foreach(_.printName)
}
NOTE:
- With the notation [D <: Donut], we are restricting only Donut types to be passed-through to the ShoppingCart class.
6. How to create instances or objects of ShoppingCart class
The example below shows how to instantiate a ShoppingCart class of type Donut.
println("\nStep 6: How to create instances or objects of ShoppingCart class")
val shoppingCart: ShoppingCart[Donut] = new ShoppingCart(Seq[Donut](vanillaDonut, glazedDonut))
shoppingCart.printCartItems
You should see the following output when you run your Scala application in IntelliJ:
Step 6: How to create instances or objects of ShoppingCart class
Vanilla Donut
Glazed Donut
NOTE:
- Assume you want a ShoppingCart of VanillaDonut but would also like to inject Donut types.
val shoppingCart: ShoppingCart[VanillaDonut] = new ShoppingCart[Donut](Seq(glazedDonut))
- You will get a compiler error when trying to have a ShoppingCart[VanillaDonut] reference a ShoppingCart[Donut]
7. How to enable contra-variance on ShoppingCart
The example below shows how to instantiate a ShoppingCart2 class of type VanillaDonut by first enabling contra-variance for Donut types on ShoppingCart2 class.
println(s"\nStep 7: How to enable contra-variance on ShoppingCart")
class ShoppingCart2[-D <: Donut](donuts: Seq[D]) {
def printCartItems: Unit = donuts.foreach(_.printName)
}
val shoppingCart2: ShoppingCart2[VanillaDonut] = new ShoppingCart2[Donut](Seq(glazedDonut))
shoppingCart2.printCartItems
You should see the following output when you run your Scala application in IntelliJ:
Step 7: How to enable contra-variance on ShoppingCart
Glazed Donut
NOTE:
- We've enabled contra-variance of type Donuts using the notation [-D <: Donut]
- In other words, you can now have a ShoppingCart of type VainllaDonut ShoppingCart[VanillaDonut] reference ShoppingCart of type Donut ShoppingCart2[Donut]
Summary
In this tutorial, we went over the following:
- How to define an abstract class called Donut
- How to extend abstract class Donut and define a case class called VanillaDonut
- How to extend abstract class Donut and define another case class of Donut called GlazedDonut
- How to instantiate Donut objects
- How to define a ShoppingCart type class which expects Donut types
- How to create instances or objects of ShoppingCart class
- How to enable contra-variance on ShoppingCart
Tip
- As we've seen in this tutorial, Scala provides support for the traditional Object Oriented approach regarding class inheritance by extending classes.
- Avoid having a case class extend other case classes. Instead, encapsulate common behaviour in an abstract class - see Scala Documentation.
- However, in Chapter 5, we will show how Scala provides greater flexibility to class inheritance by making use of mix-in with traits.
- Scala Documentation on variance.
Source Code
The source code is available on the allaboutscala GitHub repository.
What's Next
In the next tutorial, I will show you how to specify upper bounds to type parameter classes in Scala.