Type class and Bounds in Scala

Type class

Generally, when we create a class, we fix the type of parameters the class can accept. For example, in the following code, we have created a class which accepts an argument of type integer. Passing an argument of some other type will not work.

scala> class Test(a:Int) {
 | def printClass = println("my argument was "+a)
 | }

defined class Test

scala> new Test(1)
res6: Test = Test@44d70181

scala> res6.printClass
my argument was 1

scala> new Test(2).printClass
my argument was 2

scala> new Test("1")
<console>:13: error: type mismatch;
 found : String("1")
 required: Int
 new Test("1")
 ^

Type classes can take an argument of different types (type parameters). We can create both a Class and a Trait as Type class as follows

scala> class TypeClass[A](a:A)
defined class TypeClass

scala> trait TypeTrait[A]
defined trait TypeTrait

Even a function can take type arguments as follows

scala> def TypeDef[A](a:A,b:A) {}
TypeDef: [A](a: A, b: A)Unit

To create a class, trait or function which accepts type parameters, follow the function name with [A]. It is customary to use capital letters starting from A to denote type parameters. A class, trait or function could take multiple type parameters to denote that type of 1st parameter could be different from type of 2nd parameter and so on.

scala> class TypeClass[A, B](a:A, b:B) //A and B could be same or different types
defined class TypeClass

scala> new TypeClass(1,1) // invoked with both arguments as integers
res17: TypeClass[Int,Int] = TypeClass@402fdef1

scala> new TypeClass(1,"1") //invoked with one argument as integer and other as String
res18: TypeClass[Int,String] = TypeClass@69a031a4

scala> class TypeClass2[A](a:A, b:A)
defined class TypeClass2

scala> new TypeClass2(1,1) //both arguments are Integers
res19: TypeClass2[Int] = TypeClass2@4598961d

scala> new TypeClass2(1,"1") //scala expects both arguments of same type. Thus it inferred type as Any
res20: TypeClass2[Any] = TypeClass2@53202b06

Type classes are used for two purposes (a) extend the functionality of a class (b) create generic functions which can work on any types of classes.

Type classes to extend the functionality of existing classes.

You may refer to Adhoc polymorphism section of polymorphism blog to undestand how Type classes (using Traits) are used to extend the functionality of existing classes.

Type classes to create generic functions.

Say we want to create a generic function which can add an object in the middle of a List. The function should work with any type of object. We will pass the object we want to add and the list in the middle of which we want to add the object. Obviously the object and the list need to be of same type. We can use type parameter to create such a function

scala> def addInMiddle[A](a:A, b:List[A]) = { //'A' denotes that function accepts any class
| val middle = b.length/2           //find middle of the list
| val splitList = b.splitAt(middle)  //splitList will be a tuple of 2, each accessible using _1 and _2
| val newList = splitList._1:::(a::splitList._2) //create new list
| newList  //return new List
| }
addInMiddle: [A](a: A, b: List[A])List[A]

scala> val l1 = List (1,2,3)
l1: List[Int] = List(1, 2, 3)

scala> addInMiddle(4,l1) //our function works with Int
res35: List[Int] = List(1, 4, 2, 3)

scala> val l2 = List ("1","2","3") //our function works with Strings
l2: List[String] = List(1, 2, 3)

scala> addInMiddle("4",l2)
res36: List[String] = List(1, 4, 2, 3)

scala> class Dog(n:String) {
| override def toString = n
| }
defined class Dog

scala> val l = List(new Dog("d1"), new Dog("d2"))
l: List[Dog] = List(d1, d2)

scala> addInMiddle(new Dog("d3"), l) //our function works with Dog class which we have created
res34: List[Dog] = List(d1, d3, d2)

An important thing to note is that when using type parameters, you cannot use any class specific variables or functions. This is because a type class or function is mean to work with any class. A variable or function available in one class might not be available in another class. In the following example, we create a class Dog with a property name. We then create a generic function, genericPrint, to which we hope to pass Dog class and print the name. However, this will not compile as the compiler assumes that genericPrint can work with any class, for example, Dog or Integer. An Integer doesn’t have property name! Thus compiler doesn’t let it pass.

scala> class Dog (name:String) {
| val n = name
| }
defined class Dog

scala> def genericPrint[A](a:A) {
| println(a.name)
| }
<console>:12: error: value name is not a member of type parameter A
println(a.name)

If you want to create generic functions which use class specific properties then you’ll have to incorporate ‘bounds’ when you declare type classes or functions. Bounds are covered later in this blog.

Covariant, Contravariant and Invariant

Variance provides rules about how a class and its subclass/superclass can be used wih generic functions or classes. Ideally, we should be able to use a subclass in place of its superclass. This was demonstrated in the blog about polymorphism where we looked at subtyping. We know that if Class B is a subtype of Class A then we can use an instance of Class B at places which expect an instance of Class A. For example, in the following code, we create a superclass Animal and create its subclasses, Dog and Cat. We can then create a class MyPet which expects an argument of type Animal but we can provide an argument of type Dog or Cat instead.

scala> class Animal
defined class Animal

scala> class Dog extends Animal
defined class Dog

scala> class Cat extends Animal
defined class Cat

scala> class MyPet(a:Animal) //MyPet expects instance of Animal 
defined class MyPet

scala> val mp = new MyPet(new Animal)//create MyPet using Animal object
mp: MyPet = MyPet@1b0a7baf

scala> val mp2 = new MyPet(new Dog) //create MyPet using Dog (subtype of Animal)
mp2: MyPet = MyPet@2e5c7f0b

scala> val mp3 = new MyPet(new Cat) //create MyPet using Cat (subtype of Animal)
mp3: MyPet = MyPet@3d6300e8

When using Type Classes, this subtype relationship is not available by default. That is, for normal subtyping, if B is a subtype of A, then SomeClass(a:A) will take arguments of type B. When using Type classes, if a class (or a function) expects as an argument of type SomeTypeClass[A] then it will not take argument of SomeTypeClass[B] (even if B is a subtype of A).

To understand this, let us create a generic type class, Zoo and another class LondonZoo which expects an argument of type Zoo[Animal]

scala> class Zoo[A](a:A) //generic type class. This could have generic functions like add or remove animals
defined class Zoo

scala> class LondonZoo(z:Zoo[Animal]) //LondonZoo accepts Zoo of type Animal
defined class LondonZoo

We can create an instance of LondonZoo as follows

scala> val z = new LondonZoo (new Zoo[Animal](new Animal))
z: Zoo = Zoo@9e2ad91

But if we have some subtypes of Animal class, for example, Lion or Tiger, then we cannot use Zoo[Lion] and Zoo[Tiger] when instantiating LondonZoo even though Lion and Tiger are subtypes of Animal and LondonZoo accepts Zoo[Animal]

scala> class Lion extends Animal
defined class Lion

scala> class Tiger extends Animal
defined class Tiger

scala> val z = new LondonZoo (new Zoo[Lion](new Lion))
<console>:15: error: type mismatch;
found : Zoo[Lion]
required: Zoo[Animal]
Note: Lion <: Animal, but class Zoo is invariant in type A.
You may wish to define A as +A instead. (SLS 4.5)
val z = new Zoo (new Zoo[Lion](new Lion)

This default behavior is called invariant that is the type Zoo is invariant in its type parameter A.

To resolve this problem, we will have to declare class Zoo as covariant. It would tell the compiler that if B (Lion) is a subset of A (Animal) then SomeClass[B] (Zoo[Lion]) is also a subtype of SomeClass [A] (Zoo[Animal]). This will make compile accept SomeClass[B] at places where it expects SomeClass[A]

To make Zoo, change the declaration as follows by simply adding ‘+’ sign in type declaration.

scala> class Animal
defined class Animal

scala> class Zoo[+A](a:A) //made Zoo covariant by changing A to +A
defined class Zoo

scala> class Lion extends Animal
defined class Lion

scala> class Tiger extends Animal
defined class Tiger

scala> class Zoo(z:Zoo[Animal])
defined class Zoo

scala> new Zoo(new Zoo[Animal](new Animal))
res0: Zoo = Zoo@63f259c3

scala> new Zoo(new Zoo[Lion](new Lion))
res1: Zoo = Zoo@5ba88be8

scala> new Zoo(new Zoo[Tiger](new Tiger))
res2: Zoo = Zoo@31c7528f

+A means that A could be a class or its subclass.

The notation used to describe covariance is

If B <: A, then SomeClass[B] <: SomeClass[A] – If B is a subtype of A then SomeClass[B] is a subtype of SomeClass [A]. Thus just like B can be used in place of A, SomeClass[B] can be used in place of SomeClass[A]. The symbol <: denotes subtyping relationship

The contravariant relationship is opposite of covariant relationship. It is denoted as follows:

If B <: A, then SomeClass[A] <: SomeClass[B] – If B is a subtype of A then SomeClass[A] is a subtype of SomeClass [B]. Thus just like B can be used in place of A, SomeClass[A] can be used in place of SomeClass[B]. Note that we have exchanged roles of B and A in SomeClass.

Hint to remember + or – signs:

In Covariant, we accept a class or its subclass. Generally speaking,  a subclass extends the functionality of its base (super class). Thus we can say that a subclass will have extra functionality than its base class. The extra is denoted by +. Thus in Covariance, + means accept ‘A’ or ‘A’ plus something extra (subclass) (denoted by [+A])

In Contravariant, we accept a class or its superclass. Generally speaking,  a superclass will have less functionality than its subclasses. The ‘less’ is denoted by -. Thus in Contravariant, – means accept A or A minus something less (super class)(denoted by [-A])

In the following image, assuming LondonZoo takes Zoo[Lion] as argument, then if Zoo is covariant [+A], we can pass WhiteLion or Kalahari (subtypes of Lion) but not Animal (supertype of Lion) to LondonZoo. If Zoo is contravariant [-A], we can pass Animal (super class of Lion) to LondonZoo but not WhiteLion or Kalahari (subtypes of Lion).

classhierarchycovariantcontravariant

//declaring classes as per above diagram

scala> class Animal (n:String) {
 | val name = n
 | def someAnimalStuff = "some animal stuff"
 | }
defined class Animal

scala> class Lion (name:String) extends Animal (name) {
 | def someLionStuff = "some Lion stuff"
 | }
defined class Lion

scala> class Tiger (name:String) extends Animal (name) {
 | def someTigerStuff = "some Tiger stuff"
 | }
defined class Tiger

scala> class WhiteLion (name:String) extends Animal (name) {
 | def someWhiteLionStuff = "some White Lion stuff"
 | }
defined class WhiteLion

scala> class WhiteLion (name:String) extends Lion (name) {
 | def someWhiteLionStuff = "some White Lion stuff"
 | }
defined class WhiteLion

scala> class Kalahari (name:String) extends Lion (name) {
 | def someKalahariStuff = "some Kalahari stuff"
 | }
defined class Kalahari

scala> class Bengal (name:String) extends Tiger (name) {
 | def someBengalTigerStuff = "some BengalTiger stuff"
 | }
defined class Bengal

scala> class Siberian (name:String) extends Tiger (name) {
 | def someSiberianTigerStuff = "some SiberianTiger stuff"
 | }
defined class Siberian

//declare Zoo as Covariant 
scala> class Zoo[+A](a:A)
defined class Zoo

//Create Zoo with Zoo[Lion] as reference. Only Lion and subtypes of Lion are allowed as Zoo is covariant
scala> class LondonZoo(z:Zoo[Lion])
defined class ZooLondon

//we can create LondonZoo with Zoo[Lion]
scala> new LondonZoo(new LondonZoo[Lion](new Lion("lion")))
res5: Zoo = Zoo@6b85300e

//we cannot create LondonZoo with Zoo[Animal] because Animal is superclass of Lion
scala> new Zoo(new LondonZoo[Animal](new Lion("animal")))
<console>:16: error: type mismatch;
 found : Zoo[Animal]
 required: Zoo[Lion]
 new LondonZoo(new Zoo[Animal](new Lion("animal")))
 ^
//we can create LondonZoo with Zoo[WhiteLion] because WhiteLion is subclass of Lion
scala> new LondonZoo(new LondonZoo[WhiteLion](new WhiteLion("whiteLion")))
res7: LondonZoo = LondonZoo@2c1dc8e

//re-declare Zoo as contravariant. Thus only superclass of A allowed
scala> class Zoo[-A](a:A)
defined class Zoo

//Zoo with Zoo[Lion]. Only Lion and super classes of Lion are allowed as Zoo is contravariant
scala> class LondonZoo(z:Zoo[Lion])
defined class LondonZoo

//Zoo[Lion] is allowed
scala> new LondonZoo(new Zoo[Lion](new Lion("lion")))
res8: LondonZoo = LondonZoo@42561fba

//Zoo[Animal] is allowed
scala> new LondonZoo(new Zoo[Animal](new Animal("animal")))
res9: LondonZoo = LondonZoo@40147317

//Zoo[WhiteLion] is not allowed
scala> new LondonZoo(new Zoo[WhiteLion](new WhiteLion("whiteLion")))
<console>:17: error: type mismatch;
 found : Zoo[WhiteLion]
 required: Zoo[Lion]
 new LondonZoo(new LondonZoo[WhiteLion](new WhiteLion("whiteLion")))

Positions of Covariant and Contravariant arguments

Covariant and Contravariant allows you to create generic classes like Zoo which is originally declared to accept Zoo[Lion] but can take superclasses or subclasses of Lion as well as arguments. However, there are strict rules about ‘positions’ of covariant and contravariant arguments.

  • A covariant argument cannot be used as an  input argument to a function, it can only be used as a return type of the function. The exception to this rule is that a covariant argument can be used as an argument to a function if the argument has a lower bound (covered later in this blog).
  • A contravariant argument can be used as an input argument to a function but cannot be used as a return type.
//the covarient type A used a return type of zooFunction function
scala> abstract class Zoo[+A] { def zooFunction:A}
defined class Zoo

//the contravarient type A used as input argument of zooFunction function
scala> abstract class Zoo[-A] { def zooFunction(a:A)}
defined class Zoo

//failed attempted to use contravarient type A as return type of zooFunction function.
scala> abstract class Zoo[-A] { def zooFunction:A}
<console>:11: error: contravariant type A occurs in covariant position in type => A of method treat
 abstract class Zoo[-A] { def zoo:A}
 ^

//failed attempt to use covarient type A as input argument of zooFunction function
scala> abstract class Zoo[+A] { def zooFunction(a:A)}
<console>:11: error: covariant type A occurs in contravariant position in type A of value a
 abstract class Zoo[+A] { def zoo(a:A)}
 ^
//Correct used contravarient A as input and covarient type B as return type
scala> abstract class Zoo[-A,+B] { def zooFunction(a:A):B}
defined class Zoo

Explanation why covariant cannot be used in input argument position but contravariant can be used as input argument

When we pass a variable say ‘x’ of type Integer to a function ‘f’, we are limited to using methods available in Integer class (and of course the super class of Integer from which Integer class inherits). If we extend Integer class (let us call it MyInt) and define a function ‘myint’ in it, then ‘myint’ is not available in Integer class (Note: this is mere an example. You cannot extend Integer as it is declared as ‘final’). Thus there is no use in passing an object of MyInt (Integer’s subclass) to ‘f’ as we cannot use any method from MyInt in ‘f’ (the compiler won’t even know about ‘myint’ method as it can only see Integer’s methods). If, for a moment, we assume that we are allowed to use methods from MyInt (myint) in ‘f’, then what will happen when we pass an object of type Integer (we are calling a function in an object that doesn’t has that function!).

The same logic can be applied to covariant type variables (which allow subclasses, say SomeClass[MyInt] to be used in place of base class SomeClass[Int]). There is no use in allowing a covariant variable to be used as an input argument of a function as we cannot use any subtype specific methods. Scala compiler goes one step further to ensure that we do not make such a mistake. To avoid runtime errors (such as calling a method of a subclass on a variable of a base class which will never be successful),  Scala compiler does not allow using covariant type in a function argument. However, the use of contravariant function as input argument is fine because in contravariant, if MyInt is a subclass of Int, then SomeClass[Int] is a subclass of Someclass[MyInt] (note that the subtype role has reversed). Thus a contravariant function (or class) which takes SomeClass[Int] can only call methods and functions available in class Integer and thus we will not use any MyInteger specific functionality. Thus passing SomeClass[MyInt] (which is a subset of SomeClass[Int] in contravariant) to such a function (or class) is safe.

Explanation why contravariant cannot be used as return argument but covariant can be used as return argument

We can assign a value of subclass to a variable of superclass but not the other way around.

scala> class Animal
defined class Animal

scala> class Dog extends Animal
defined class Dog

scala> val d:Dog =new Animal //assinging superclass object to subclass variable is not allowed
<console>:13: error: type mismatch;
found : Animal
required: Dog
val d:Dog =new Animal
^

scala> val d:Animal =new Dog //assigning subclass object to superclass variable is allowed
d: Animal = Dog@5918c260

Extending above rule for type classes, if Zoo[-A] is contravariant and ‘A’ could be returned as a return value in some function ‘f’ (say we return a List[A]), then assuming Lion is a subset of Animal, ‘f’ could return List[Animal] instead of List[Lion] (because in contravariant, if Lion is a subset of Animal then SomeClass[Animal] is a subset of SomeClass[Lion]. Thus the following statement becomes possible but it shouldn’t be

scala> class Animal
defined class Animal

scala> class Lion extends Animal
defined class Lion

scala> class Zoo[-A] { //A is contravariant
| def zooFunction:List[A] = List[A]() //assume this compiles but it will not because we cannot use contravariant value as return type
| }
defined class Zoo

//if compiler doesnt provide contravariant check, then following line becomes possible where we assign a super class object to subclass variable

scala> val p:List[Lion]=(new Zoo[Animal]).zooFunction

The correct implementation is to declare A as covariant so that we can return a List[A]

scala> class Zoo[+A] { //A is covariant
| def zooFunction:List[A] = List[A]() //this will compile as covariant can be returned
| }
defined class Zoo

// assigning List[Animal] (return type) to variable 'p' which is also of type List[Animal] -OK

scala> val p:List[Animal]=(new Zoo[Animal]).zooFunction
p: List[Animal] = List()

// assigning List[Lion] (return type) to variable 'p' which is also of type List[Lion] -OK

scala> val p:List[Lion]=(new Zoo[Lion]).zooFunction
p: List[Lion] = List()

// assigning List[Lion] (return type) to variable 'p' which is of type List[Animal] -OK because we are assigning object of subclass to variable of superclass

scala> val p:List[Animal]=(new Zoo[Lion]).zooFunction
p: List[Animal] = List()

// assigning List[Animal] (return type) to variable 'p' which is also of type List[Lion] -Not OK. Cannot assign object of supertype to variable of subtype

scala> val p:List[Lion]=(new Zoo[Animal]).zooFunction
<console>:14: error: type mismatch;
found : List[Animal]
required: List[Lion]
val p:List[Lion]=(new Zoo[Animal]).zooFunction
^

Example showing that Scala will not allow using contravariant as return type

scala> class Zoo[-A] {
| def zooFunction:List[A] = List[A]()
| }
<console>:12: error: contravariant type A occurs in covariant position in type => List[A] of method zooFunction
def zooFunction:List[A] = List[A]()

NOTE:

  • Contravariant goes from specialization to generalization.  Use Contravariant when you class has some maximum criteria.
  • Covariant goes from generalization to specialization. Use Covariant when your class has some minimum criteria.

Examples

  • A surgeon may advise general medicine (say high fever) but a general practitioner cannot advise on surgery. Thus if we have a class hierarchy Doctor -> General -> Surgeon->Brain Surgeon, then we may declare class EmergencyWard as EmergencyWard(EmergencyDoctor[Surgeon]) and declare EmergencyDoctor as covariant i.e. EmegencyDoctor[+T] as we need the expertise of at least a Surgeon who can handle surgery and also general medicine.
  • A general garbage bin can be used for any type of waste (paper, plastic, general rubbish) but a recycle garbage bin cannot be used for general waste. A general household requires at least a general rubbish bin but can have a recycle bin as well. Thus it might be sufficient to provide a recycle bin which could be used for recycling or general waste. Thus if we have a class hierarchy RubbishBin -> RecycleBin -> PlasticBottlesBin ->AnyPlasticRubbishBin, then we can create a class HouseholdRubbish as HouseholdRubbish(HouseholdRubbishBin[RecycleBin]) and declare HouseholdRubbishBin as contravariant (HouseholdRubbishBin[-T]) as we do not need very specialized bins for plastic bottles or any plastic item.
  • A Museum about Lions can contain information about all species of Lions (subtypes of Lions) but not all general animals. A class Museum could be created as Museum(AnimalMuseum[Lion]) where AnimalMuseum is defined as AnimalMuseum[+A] (Covariant) because we want at least Lion class and will also accept its subtypes.

At the beginning of this section, I mentioned that ideally, we should be able to use a subclass in place of its superclass. Contravariant and Covariant definitions define what should be considered a subclass of a class when using type classes. In Covariant, if B<:A then SomeClass[B] <: SomeClass[A] and thus SomeClass[B] can be used in place of SomeClass[A]. In contravariant, if B<:A then SomeClass[A] <: SomeClass[B] and thus SomeClass[A] can be used in place of SomeClass[B].

Bounds

Upper Bounds

Contravariant and Covariant types helped us define subtypes for type classes. Consider following Class hierarchy.

lowerupperbound

For a type class Zoo[A], we want that ‘A’ can only be either WhiteLion or WhiteLion’s subclasses (SomeWhiteLionSubclass). In other words, Zoo[Lion] and Zoo[Animal] should not be allowed. To do this, we can create an Upper bound on ‘A’ by creating Zoo as Zoo[A<:WhiteLion]. ‘A’ can be WhiteLion or its subtype. This restriction would allow us to use any methods available in the class used to define the upper bound (WhiteLion) in Zoo as all the subclass of WhiteLion would have these methods available. Note that without Upper bound, we couldn’t use any class specific methods in Zoo because ‘A’ could be of any type and may not have the method we use in Zoo defined in it.

scala> class Animal {
 | def someAnimalFunction = {"some Animal Function"}
 | }
defined class Animal

scala> class Lion extends Animal {
 | def someLionFunction = {"some Lion Function"}
 | }
defined class Lion

scala> class WhiteLion extends Lion {
 | def someWhiteLionFunction = {"some WhiteLionFunction"}
 | }
defined class WhiteLion

scala> class SomeWhiteLionSubclass extends WhiteLion {
 | def someSomeWhiteLionSubclassFunction = {"some SomeWhiteLionSubclass function"}
 | }
defined class SomeWhiteLionSubclass


//without upperbound, we cannot use class specific functions as 'A' can be of any type

scala> class Zoo[A] (a:A) {
 | def someZooFunctin = a.someWhiteLionFunction
 | }
<console>:12: error: value someWhiteLionFunction is not a member of type parameter A
 def someZooFunctin = a.someWhiteLionFunction

//with Upperbound, we can safely use methods of UpperBound class

scala> class Zoo[A<:WhiteLion] (a:A) {
 | def someZooFunction = a.someWhiteLionFunction
 | }
defined class Zoo

//can instantiate Zoo with WhiteLion (the upperbound)

scala> (new Zoo[WhiteLion](new WhiteLion)).someZooFunction
res2: String = some WhiteLionFunction

//can instantiate Zoo with subclass of WhiteLion. Note that  someWhiteLionFunction is available in SomeWhiteLionSubclass because of inheritence 

scala> (new Zoo[SomeWhiteLionSubclass](new SomeWhiteLionSubclass)).someZooFunction
res3: String = some WhiteLionFunction

//cannot instantiate Zoo with Lion as only Lion is a super class (base class) of WhiteLion

scala> (new Zoo[Lion](new Lion)).someZooFunction
<console>:16: error: type arguments [Lion] do not conform to class Zoo's type parameter bounds [A <: WhiteLion]
 (new Zoo[Lion](new Lion)).someZooFunction
 ^

Lower Bounds

Consider another scenario in which we want ‘A’ can only be either SomeKalahariSubclass or its base classes (Kalahari, Lion, Animal) but not subclass of SomeKalahariSubclass (i.e. SubclassOfSomeKalahariSubclass should not work).  To do this, we can create a Lower bound on ‘A’ by creating Zoo as Zoo[A>:SomeKalahariSubclass].

The purpose of using Lower bound is to give compiler flexibility to infer types as it finds suitable. As an example, we discussed earlier that a covariant type cannot be used as an input argument because it could lead to runtime errors. Scala compiler enforces strict rules which doesn’t allow the use of covariant type as input arguments. Instead of living with such strict rule, we could let the compiler know that our input variable will follow certain rules which would allow the compiler to find safe ways to execute our code. In following example, function zooFunction is a covariant function which works on any type of ‘A’. It takes an object of type A and List of type A and returns a new list. It is able to take covariant argument because we define a lower bound for A. Thus, when the compiler notices that we passed object of type zoo[WhiteLion] and a list of type zoo[SomeWhiteLionSubclass], it uses the lower bound restriction (WhileLion) and returns a list of type WhiteLion. Without lower bound rule, the compiler wouldn’t know whether to return List of WhiteLion or Lion or SomeWhilteLionSubclass (in fact, it wouldn’t have compiled the code).

//unable to use convariant A without Lower bound

scala> class Zoo[+A] {
| def zooFunction(a:A, list:List[A]):List[A] = {a::list}
| }
<console>:12: error: covariant type A occurs in contravariant position in type A of value a
def zooFunction(a:A, list:List[A]):List[A] = {a::list}
^
<console>:12: error: covariant type A occurs in contravariant position in type List[A] of value list
def zooFunction(a:A, list:List[A]):List[A] = {a::list}
^

// used A (covariant) as input argument in function zooFunction using lower bound

scala> class Zoo[+A] {
| def zooFunction[A >: SomeWhiteLionSubclass](a:A, list:List[A]):List[A] = {a::list}
| }
defined class Zoo

//In these examples, we add new objects to List. The compiler keeps changing type of List such that its use is safe

//Initial list created of type SomeWhiteLionSubclass

scala> val list = List(new Zoo[SomeWhiteLionSubclass])
list: List[Zoo[SomeWhiteLionSubclass]] = List(Zoo@188b6035)

//changed list type to WhiteLion because the next object we add is of type WhiteLion. As If list stays of type SomeWhiteLionSubclass then we cannot store WhiteLion object in it. Thus compiler changed its type

scala> val list = (new Zoo[WhiteLion])::List(new Zoo[SomeWhiteLionSubclass])
list: List[Zoo[WhiteLion]] = List(Zoo@7cca01a8, Zoo@462abec3)

//changed list type to Lion because the next object we add is of type Lion.

scala> val list = (new Zoo[Lion])::(new Zoo[WhiteLion])::List(new Zoo[SomeWhiteLionSubclass])
list: List[Zoo[Lion]] = List(Zoo@7fe07361, Zoo@741ac284, Zoo@4ef4f627)

scala> val list = (new Zoo[Animal])::(new Zoo[Lion])::(new Zoo[WhiteLion])::List(new Zoo[SomeWhiteLionSubclass])
list: List[Zoo[Animal]] = List(Zoo@48ccbb32, Zoo@36551e97, Zoo@685d7ba5, Zoo@16d41725)

//we are even able to add String object to list. The compiler changed the type to Object 

scala> val list = (new Zoo[String])::(new Zoo[Animal])::(new Zoo[Lion])::(new Zoo[WhiteLion])::List(new Zoo[SomeWhiteLionSubclass])
list: List[Zoo[Object]] = List(Zoo@f48a080, Zoo@5f7cd50e, Zoo@401ec794, Zoo@d76099a, Zoo@47f0f414)

Following is another example of Upper bound which would help us understand View bounds (covered later). Say we want to create a generic function ‘Smaller’ which takes two arguments of the same type and returns the smaller of the two arguments. Such a function can be defined as follows (I’ll intentionally make errors to show how to create such a generic function)

This fails because we use ‘<‘ to compare arg1 and arg2 but as A can be of any type, A may not have ‘>’ defined. Thus compiler doesn’t let us use ‘>’

scala> def Smaller[A](arg1:A, arg2:A) = {
| if (arg1<arg2) arg1
| else arg2
| }
<console>:12: error: value < is not a member of type parameter A
if (arg1<arg2) arg1

As we want to compare two things, we can use only those classes which extend Comparable trait. Thus any class which extends Comparable (and thus has a way to determine if its value is smaller, larger or equal) can be used with Smaller function. String is one such class

scala> def Smaller[A <: Comparable[A]](arg1:A, arg2:A) = {
 | if (arg1.compareTo(arg2) < 0) arg1 else arg2
 | }
Smaller: [A <: Comparable[A]](arg1: A, arg2: A)A

scala> Smaller("3","2")
res9: String = 2

View Bounds

If you try the Smaller function with Integer values, it will fail as Integers do not extend Comparable

scala> Smaller(1,2)
<console>:13: error: inferred type arguments [Int] do not conform to method Smaller's type parameter bounds [A <: Comparable[A]]
 Smaller(1,2)
 ^
<console>:13: error: type mismatch;
 found : Int(1)
 required: A
 Smaller(1,2)
 ^
<console>:13: error: type mismatch;
 found : Int(2)
 required: A
 Smaller(1,2)

The Upper bound is a strict type of bound. It means that if a class ‘X’ is defined as a Upper bound for a type parameter A, then only objects of type X or objects of sub classes of ‘X’ are allowed to represent ‘A’. View bounds are less restrictive. Using View bounds, objects of any class (say C even if C is not a base class of X) could represent ‘A’ as long as there is an implicit conversion available from C to X.

To make Smaller work with Integers, we can use Ordered trait instead of Comparable. Ordered trait defines < method along with other methods which could be used for comparison (eg >, >=, <, <=). Integers have an implicit conversion from Integers to Rich Integers which in turn extend Ordered trait (so does Strings). Thus we will be able to use String and Integer with Smaller

scala> def Smaller[A <% Ordered[A]](arg1:A, arg2:A):A = {
| if (arg1 < arg2) arg1
| else arg2
| }
Smaller: [A](arg1: A, arg2: A)(implicit evidence$1: A => Ordered[A])A

The <% symbol tells scala compiler to allow objects which either extend Ordered (are subtypes of Ordered) or which can be implicitly converted to a class which extends Ordered. This way, we are able to access methods of Ordered in ‘A’. Thus view bounds are used when we want to use methods of class B in class A provided there is a conversion available from A to B.

scala> Smaller(3,2)
res2: Int = 2

scala> Smaller("3","2")
res3: String = 2

A section on ad-hoc polymorphism using implicit conversion in blog on polymorphism discusses that a view bound is nothing but a syntax sugar of implicit conversions

Context bound

While a view bound uses implicit conversion (A=>B[A]), a context bound uses implicit value (A:B[A]). A section on ad-hoc polymorphism using type classes in blog on polymorphism discusses that a context bound is nothing but a syntax sugar of implicit conversions. The word ‘context’ signifies that we are using implicit value/parameter as implicit values are used within some context (discussed in blog on polymorphism)

Other types of Bound

  • A =:= B or =:=[A,B]

 

When used as A =:=<some type>, this indicates that A must be of the same type as <some type>. Enforcing such a bound would ensure that our function works for arguments of <some type> and not with types which could be derivates of <some type>. For example, a character could be interpreted as an integer. Thus following function works with both integer and characters.

//simple increment function

scala> def incrementIntOnly(x:Int)= {1+x}
incrementIntOnly: (x: Int)Int

//works with integer

scala> incrementIntOnly(1)
res21: Int = 2

// works with character also

scala> incrementIntOnly('c')
res23: Int = 100

// enforce that 'A' must be an integer using =:=

scala> def incrementIntOnly[A](x:A)(implicit ev:A=:=Int)= {1+x}
incrementIntOnly: [A](x: A)(implicit ev: =:=[A,Int])Int

//works with integer

scala> incrementIntOnly(1)
res24: Int = 2

//doesnt works with character

scala> incrementIntOnly('c')
<console>:19: error: Cannot prove that Char =:= Int.
 incrementIntOnly('c')
 ^

//another example
//implicit tells compiler to look for an implicit parameter which converts
//from type A to type Double
def floatOnly[A](x:A)(implicit ev:A =:= Double) = {
  x.toInt
}

floatOnly(1.0)
//this will not compile
floatOnly (1)
  • A <%< B or <%<[A,B]

When used as A <%< <some type>, this indicates that A must be viewable as <some type>.

  • A <:< B or <:< [A,B]

When used as A <:< <some type>, this indicates that A must be subtype of  <some type>. For example, following function works because we have put a constraint that the passed parameter’s type is a subtype of Number.

//implicit tells compiler to look for a implicit parameter which converts
//from type A to another type which is a subtype of Numeric. 
// This is defined in scala.Predef

def numberOnly[A](x:A)(implicit ev: A <:< Number)= {
  x.doubleValue()
}
numberOnly: numberOnly[A](val x: A)(implicit val ev: <:<[A,Number]) => Double

numberOnly[java.lang.Integer](1)

//this will not compile, complaining that String is not a Number
numberOnly[String]("")

The above two do not work from scala 2.9. onwards. You’ll get compilation error if you use them in 2.10

2 thoughts on “Type class and Bounds in Scala

Leave a comment