Journey of Implicits in Scala — part 2

ayush mittal
7 min readMar 8, 2022

Type Annotations in Scala

Photo by Mark Duffel on Unsplash

This is the second part of a 5 part series to understand the concept of implicits in Scala . The road taken is as follows

  1. Introduction to implicit methods or variable definitions and method parameter lists.
  2. Type Annotations in Scala
  3. Type classes in Scala
  4. Implicit hell and how painfull it really is?
  5. Scala 3’s approach to Type Classes.

In this part we will understand how to use implicits are crucial in delivering the famous type-class design pattern in Scala.

What do we mean by type

We often come across syntax like this in a codebase

def method[A <: B](b :B) case class Container[+T]case class Printer[-T]case class Zoo[A <% Animal]case class Box[A: Animal]

All these syntax’s are forms of type annotation. Before we understand how to decipher their meanings we must understand what is a type ? Is a type in synonyms for class ? What does it means to have a type ?

Type is a set of things which share same characteristics. Members of a type belong to that set. class or interfaces or traits are ways to create members of that set.

So if we define a class called Animal, we are basically saying — here is a Set which is named Animal. And all the instances that can be created by calling new on the Animal class, are the acceptable values within this Animal Set. The idea is to look classes as one way to fill or create acceptable value for that set. But there are other ways also to fill that set. Like enumerations .

object WeekDay extends Enumeration {
type WeekDay = Value
val Mon, Tue, Wed, Thu, Fri, Sat, Sun = Value
}

There is no class defined here. Instead we have a type(set) named WeekDay which is populated by all members of the enum WeekDay. So type and class is not synonymous. classes are one way of presenting a type.

Lets look at some examples to apply this definition of viewing types as set

class Dog(name: String)val dog1 = new Dog ("Scooby")
val dog2 = new Dog ("Doofy")
..

See the Dog class not as a blueprint for creating instances of Dog but as a way to outline a type, a set which will only contain Dog objects.

case class Box[T]

What type does class Box[T] create? It could be a Box[Chocolates] or a Box[Dog]. Clearly Box[T] by itself does not imply any type. We need to specify T to establish a type. Supplying a Dog for T outlines a type Box[Dog]. Similarly a Box[Chocolate] , Box[Juice] etc etc. Each new T defines a new type aka a new set. Because of these reasons a Box[T] is known as a Type Constructor. They allows us to create concrete types by filling their holes. The types that fill the holes provided by a Type constructor are referred to as Type parameters. Dog is a type parameter in Box[Dog] just as Chocolate is a type parameter in Box[Chocolate].

Scala provides us with mechanisms to “annotate” these type parameters. By annotating type parameters, we specify certain characteristics these type parameters can have. So when we see

Box[+Dog]
Box[-Dog]
Box[Dog <% Animal]
Box[Dog: Animal]

we are specifying a type parameter and we are also adding type annotations.

Type Annotations

  1. Type bound annotation

A definition of a type constructor like Box[T] basically means that T can be anything. There are no restrictions on what the type parameter can be. If we wish that T is acceptable only if it is a subtype of another specified type then we are setting the upper bound on T. Scala uses the syntax
T <: UpperBoundType to denote this. Let’s say we want only Box of Goods types can be created.

scala> class Goodsscala> case class Chocolates(name: String) extends Goodsscala> case class Mangos(name: String) extends Goodsscala> case class Human(name: String)scala> case class Box[T <: Goods](thing: T)scala> Box(Chocolates("yummy"))val res6: Box[Chocolates] = Box(Chocolates(yummy))scala> Box(Mangos("tasty"))val res7: Box[Mangos] = Box(Mangos(tasty))scala> Box(Human("name"))^error: inferred type arguments [Human] do not conform to method apply's type parameter bounds [T <: Goods]^error: type mismatch;found   : Humanrequired: T

The repl above shows that since Human is not a subtype of Goods a Box[Human] type cannot be created.

Scala also support a T >: LowerBoundType annotation to set the lower bound. The type parameter T must be a subtype of LowerBoundType

2. Variance relationship annotations

Specifying a type parameter creates a new concrete type. Variance rules define the impact of subtype relationship in type parameters on the relationship between the new concrete types. So if Dog is a subtype of Animal that what is the relationship between Box[Dog] and Box[Animal].

  • Covariance Annotation

Covariance relationship means that if you have two types that are subtypes, their subtype relationship carries over to the type they create when they are used as type parameters.

For example, if Dog is a Subtype of Animal, then Box of dog: Box[Dog] is a subtype of Box of animal:Box[Animal] . The plus symbol + is used to annotate that the relationship is covariance.

So, for example the definition of the Box type constructor will look like Box[+T]

  • Contravariance Annotation

Contravariance is the reverse of covariance relationship. It means that the subtype relationship between type parameter is reversed for the types they are used to construct.

In a contravariant case, if we have Dog as a subset of Animal, then Box[Animal] is a subset of Box[Dog]. This relationship is annotated using minus symbol -

Thus the definition of Box with a contravariant relationship on its type parameter would look like Box[-T]

  • Invariant Annotation

This means that there is no relationship between the subtype relationship of type parameters and the type they create.

This means that even if Dog is a subtype of Animal, it does not mean Box[Dog] is a subtype of Box[Animal]. With invariance relationship, Box[Animal] is a separate distinct type from Box[Dog] even if Dog is a subtype of Animal.

There is no special annotation to represent invariance. Hence Box[T] indicates invariance. Invariance is the default case.

Check this blog to understand more about variance.

3. View bound annotations

Now we will bring the protagonist back into the story i.e Implicits. View bound annotation are specified as follows

class TypeConstructor[TypeParameter <% AnotherType](tipe: TypeParameter)

The TypeParameter <% AnotherType annotation syntax requires that there must be an implicit value in scope to convert from TypeParameter to AnotherType.

Here is a code snipper for clarity —

scala> case class Number(number: String)scala> case class Box[A <% Number](a : A)scala> Box(1)^error: No implicit view available from Int => Number.scala> implicit def intToNumber(int: Int): Number = Number(int.toString)def intToNumber(int: Int): Numberscala> Box(1)val res1: Box[Int] = Box(1)

In the repl we define a class Number which takes a String argument. We then specify type annotation Box[A <% Number] which means that whatever type parameter A is used to construct a new type, there must be an implicit conversion available from A => Number. So another way of writing the Box definition would be

case class Box[A](a: A)(implicit ev: A => Number)

and this is actually the format that Scala compiler recommends since view bounds are deprecated.

4. Context bound annotations

The context bound indicates that for a Type parameter A, there should be a value in implicit scope, that has been created using another type constructor say B but by passing A as the type parameter of this other type constructor B. Sounds complex , lets see an example first

trait Animal[A] {
def eat: String
}
case class Dog(name:String)
case class Cat(name:String)

implicit val dogAnimal: Animal[Dog] = new Animal[Dog] {
override def eat: String = "dog food"
}
implicit val catAnimal: Animal[Cat] = new Animal[Cat] {
override def eat: String = "cat food"
}

case class Box[A : Animal](animal:A) {
def eatFromBox = {
implicitly[Animal[A]].eat
}
}

// prints dog food
Box(Dog("dog")).eatFromBox
// prints dog food
Box(Cat("cat")).eatFromBox

The syntax for defining Box is[A : Animal]. It means when we supply a type parameter to fill the hole of Box i.e A, then there must be a Animal[A] in implicit scope. If we say Box[Dog] then an implicit Animal[Dog] must be available in scope

So the general syntax is

trait TypeConstructor[A : B]
  • TypeConstructor is a type constructor that takes A as a type parameter
  • For Whatever A that is provided, there must exist a value of B[A] in the implicit scope.

So we can see that context bounds are a shortcut (syntactic sugar) for expressing

trait TypeConstructor[A,B](implicit ev: B[A])

And if you look closely at the Box[A : Animal] code snippet, we have basically performed operations on type A i.e call the method eat without having any relationship between Animal, Dog and Cat. The only requirement being that for whatever type A is, there must be a Animal[A]where the Animal[A] represents the required evidence needed in other to perform a polymorphic operation on A i.e Dog or Cat.

This feature of type constructors with context bounds, that allow us to perform polymorphic operations with using overloading or overriding is quite special. This is the feature of type-classes in Scala and we will explore this in detail in the next blog.

Further readings

--

--