Journey of Implicits in Scala — part 2
Type Annotations in Scala
This is the second part of a 5 part series to understand the concept of implicits in Scala . The road taken is as follows
- Introduction to implicit methods or variable definitions and method parameter lists.
- Type Annotations in Scala
- Type classes in Scala
- Implicit hell and how painfull it really is?
- 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
- 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 takesA
as a type parameter- For Whatever
A
that is provided, there must exist a value ofB[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