CHAPTER 8

image

Scala Type System

Types in a programming language are checked at compile time and can be inferred by a compiler. Scala has a strong and statically typed language with a unified Type system. The two fundamental design considerations of a programming language are static versus dynamic typing and strong versus weak typing.

In static typing, a variable is bound to a particular type. In dynamic typing, the type is bound to the value instead of the variable. Scala and Java are statically typed languages, whereas JavaScript, Python, Groovy, and Ruby, are dynamically typed languages.

If a type is static and strongly typed, every variable must have a definite type. If a type is dynamic and strongly typed, every value must have a definite type. However, in the case of weak typing, a definite type is not defined; Scala, Java, and Ruby are principally strongly typed languages. Some languages, such as C and Perl, are weakly typed.

Scala brings the best of two worlds, in that it feels like a dynamically typed language, because of type inference, and at the same time, Scala gives you all the benefits of static typing in terms of an advanced object model and an advanced type system.

This chapter explores venues such as which type parameters should be covariant, contravariant, or invariant under subtyping, using implicits judiciously, and so forth.

Unified Type System

Scala has a unified type system, enclosed by the type Any at the top of the hierarchy and the type Nothing at the bottom of the hierarchy, as illustrated in Figure 8-1. All Scala types inherit from Any. The subtypes of Any are AnyVal (value types, such as Int and Boolean) and AnyRef (reference types, as in Java). As you can see in the Figure 8-1, the primitive types of Java are enclosed under AnyVal and, unlike Java, you can define your own AnyVal. And also unlike Java, Scala does not have Wrapper Types, such as Integer, to be distinguished from the primitive type, such as int.

9781484202333_Fig08-01.jpg

Figure 8-1. Unified object model

As you can see in the Figure 8-1, Any is a supertype of both AnyRef and AnyVal.AnyRef corresponds to java.lang.Object, and is the supertype of all objects. AnyVal on the other hand represents the value such as int and other JVM primitives. Because of this hierarchy, it becomes possible to define methods that take Any, thus being compatible with both scala.Int instances as well as java.lang.String (see Listing 8-1).

Listing 8-1. Using Any

import scala.collection.mutable.ListBuffer
val list = ListBuffer[Any]()
val x= 2
list += x
class Book
list += new Book()

In Listing 8-1, Book extends AnyRef, and x is an Int that extends AnyVal.

scala> import scala.collection.mutable.ListBuffer
import scala.collection.mutable.ListBuffer
scala> val list = ListBuffer[Any]()
list: scala.collection.mutable.ListBuffer[Any] = ListBuffer()
scala> val x= 2
x: Int = 2
scala> list += x
res12: list.type = ListBuffer(2)
scala> class Book
defined class Book
scala> list += new Book()
res13: list.type = ListBuffer(2, Book@15e8485)

You can limit a method to only be able to work on Value Types as seen in Listing 8-2.

Listing 8-2. Value Types

def test(int: AnyVal) = ()
test(5)
test(5.12)
test(new Object)

In Listing 8-2, test(5) takes an Int that extends AnyVal and test(5.12) takes a Double that also extends AnyVal. Test(new Object) takes an Object that extends AnyRef. Refer to Figure 8-1. Test(new Object) fails to compile.

scala> def test(int: AnyVal) = ()
test: (int: AnyVal)Unit
scala> test(5)
scala> test(5.12)
scala> test(new Object)
<console>:9: error: type mismatch;
 found   : Object
 required: AnyVal
Note that implicit conversions are not applicable because they are ambiguous: both method ArrowAssoc in object Predef of type [A](self: A)ArrowAssoc[A]and method Ensuring in object Predef of type [A](self: A)Ensuring[A] are possible conversion functions from Object to AnyVal
              test(new Object)
                   ^

The idea is that this method will only take Value Classes, be it Int or your own Value Type. So, we imply Java code is not as type-safe as Scala code. You’re probably thinking, “But Java is a statically typed language, doesn’t it give me all the safety that Scala does?” The answer to that is no. Take a look at the Listing 8-3 and spot the problem:

Listing 8-3. Java’s Type Unsafety

public class Bad {
public static void main(String[] argv) {
Object[] a = argv;
a[0] = new Object();
}
}

This is legal Java code, and here’s what happens when we run the code:

> java Bad Hello
Exception in thread "main" java.lang.ArrayStoreException: java.lang.Object
at Bad.main(Bad.java:4)

Java allows us to assign a String[] to Object[]. This is because a String is a subclass of Object, so if the array was read-only, the assignment would make sense. However, the array can be modified. The modification that we’ve demonstrated shows one of Java’s “type-unsafety” features. We’ll discuss why this happened and the complex topic of invariant, covariant, and contravariant types later in this chapter. Let’s start looking at how Scala makes the architect’s job easier and also makes the coder’s job easier.

Type Parameterization

Scala’s parameterized types are similar to generics in Java. If you are familiar with Java or C# you might already have some understanding of parameterized types. Scala’s parametrized types provide the same features as Java generics, but with extended functionalities.

Image Note  Classes and traits that take type parameters are called generic; the types they generate are called parameterized type.

One straightforward syntactical difference is that Scala uses square brackets ([...]), while Java uses angle brackets (<...>). For example, a list of strings would be declared as shown in Listing 8-4.

Listing 8-4. Scala List of Strings

val list : List[String] = List("A", "B", "C")

Scala allows angle brackets to be used in the method name. So, to avoid ambiguities, Scala uses square brackets for parameterized types.

Types in Scala are used to define classes, abstract classes, traits, objects, and functions. Type parameterization lets you make these generic. As an example, sets can be defined as generic in the following manner: Set[T]. However, unlike Java which allows raw types, in Scala you are required to specify type parameters, that is to say, the Set[T], is a trait, but not a type because it takes a type parameter.

As a result, you cannot create variables of type Set as illustrated in Listing 8-5.

Listing 8-5. Scala Requires to Specify Type Parameters

def test(s: Set) {} // this will not compile
scala> def test(s: Set) {}
<console>:14: error: type Set takes type parameters
       def test(s: Set) {}
                   ^

Instead, trait Set enables you to specify parameterized types, such as Set[String], Set[Int], or Set[AnyRef] as in Listing 8-6.

Listing 8-6. Specifying Parameter Types

def test(s: Set[AnyRef]) {}
scala> def test(s: Set[AnyRef]) {}
test: (s: Set[AnyRef])Unit

For example, trait Set in Listing 8-6 defines a generic set where the specific sets are Set[Int] and Set[String], and so forth. Thus, Set is a trait, and Set[String] is a type. The Set is a generic trait.

Image Note  In Scala, List, Set, and so on could also be referred as a type constructors, because they are used to create specific types. You could construct a type by specifying a type parameter. For example, List is the type constructor for List[String] and List[String] is a type. While Java allows raw types, Scala requires that you specify type parameters and does not allow you to use just a List in the place of a type, as it’s expecting a real typenot a type constructor.

In the light of inheritance, type parameters raise an important question regarding whether Set[String] be considered a subtype of Set[AnyRef]. That is, if S is a subtype of type T, then should Set[S] be considered a subtype of Set[T]? Next you will learn a generic type concept that defines the inheritance relation and answers the aforementioned question.

Variance

Variance defines inheritance relationships of parameterized types, which brings to light whether a Set[String], for example, is a subtype of Set[AnyRef]. A declaration like class Set[+A] means that Set is parameterized by a type A. The + is called a variance annotation.

Variance is an important and challenging concept. It defines the rules by which parameterized types can be passed as parameters. In the beginning of the chapter, we showed how passing a String[] (Java notation) to a method expecting an Object[] can cause problems. Java allows you to pass an array of something to a method expecting an array of something’s superclass. This is called covariance. On the surface, this makes a lot of sense. If you can pass a String to a method expecting an Object, why can’t you pass an Array[String] (Scala notation) to a method expecting an Array[Object]? Because Array is mutable; it can be written to in addition to being read from, so a method that takes an Array[Object] may modify the Array by inserting something that cannot be inserted into an Array[String].

Defining the type variance for type parameters allows you to control how parameterized types can be passed to methods. Variance comes in three flavors: invariant, covariant, and contravariant. Type parameters can be individually marked as covariant or contravariant and are by default invariant. Variance in Scala is defined by using + and - signs in front of type parameters.

Covariant Parameter Types

Covariant parameter types are designated with a + before the type parameter. A covariant type is useful for read-only containers. Scala’s List is defined as List[+T], which means that it’s covariant on type T. List is covariant because if you pass a List[String] to a method that expects a List[Any], then every element of the List satisfies the requirement that is an Any and we cannot change the contents of the List. Figure 8-2 gives a very clear picture of Covariance, i.e. if S extends T then Class[S] extends Class[T].

9781484202333_Fig08-02.jpg

Figure 8-2. Covariance in Scala

Image Tip  Covariance: If S extends T then Class[S] extends Class[T].

Let’s define an immutable class, Getable (see Listing 8-7). Once an instance of Getable is created, it cannot change, so we can mark its type, T, as covariant.

Listing 8-7. Immutable Class Getable

class Getable[+T](val data: T)
scala> class Getable[+T](val data: T)
defined class Getable

Let’s define a method that takes a Getable[Any] (see Listing 8-8).

Listing 8-8. Defining a Method That Takes a Getable

def get(in: Getable[Any]) {println("It's "+in.data)}
scala> def get(in: Getable[Any]) {println("It's "+in.data)}
get: (Getable[Any])Unit

We define an instance of Getable[String] in Listing 8-9.

Listing 8-9. Instance of Getable

val gs = new Getable("String")
scala> val gs = new Getable("String")
gs: Getable[java.lang.String] = Getable@10a69f0

We can call get with gs:

scala> get(gs)
It's String

Let’s try the same example but passing a Getable[java.lang.Double] into something that expects a Getable[Number] (see Listing 8-10).

Listing 8-10. Passing Double

def getNum(in: Getable[Number]) = in.data.intValue
def gd = new Getable(new java.lang.Double(33.3))
getNum(gd)
scala> def getNum(in: Getable[Number]) = in.data.intValue
getNum: (Getable[java.lang.Number])Int
scala> def gd = new Getable(new java.lang.Double(33.3))
gd: Getable[java.lang.Double]
scala> getNum(gd)
res7: Int = 33

Yes, the covariance works the way we expect it to. We can make read-only classes covariant. That means that contravariance is good for write-only classes.

Contravariant Parameter Types

So, if covariance allows us to pass List[String] to a method that expects List[Any], what good is contravariance? Contravariance indicates if S extends T, then Class[T] extends Class[S] as illustrated in Figure 8-3.

9781484202333_Fig08-03.jpg

Figure 8-3. Contravariance in Scala

Image Tip  Contravariance: If S extends T then Class[T] extends Class[S].

Let’s first look at a write-only class, Putable (see Listing 8-11).

Listing 8-11. Putable Class

scala> class Putable[-T] {
def put(in: T) {println("Putting "+in)}
}

Next, let’s define a method that takes a Putable[String]:

scala> def writeOnly(in: Putable[String]) {in.put("Hello")}
writeOnly: (Putable[String])Unit

And let’s declare an instance of Putable[AnyRef]:

scala> val p = new Putable[AnyRef]
p: Putable[AnyRef] = Putable@75303f

And what happens if we try to call writeOnly?

scala> writeOnly(p)
Putting Hello

Okay, so we can call a method that expects a Putable[String] with a Putable[AnyRef] because we are guaranteed to call the put method with a String, which is a subclass of AnyRef. Standing alone, this is not particularly valuable, but if we have a class that does something with input that results in output, the value of contravariance becomes obvious.

The inputs to a transformation are contravariant. Calling something that expects at leastany AnyRef with a String is legal and valid. But the return value can be covariant because we expect to get back a Number, so if we get an Integer, a subclass of Number, we’re okay. Let’s see how it works. We’ll define DS with a contravariant In type and a covariant Out type:

scala> trait DS[-In, +Out]{def apply(i: In): Out}
defined trait DS

Let’s create an instance that will convert Any into an Int:

scala> val t1 = new DS[Any, Int]{def apply(i: Any) = i.toString.toInt}
t1: java.lang.Object with DS[Any,Int] = $anon$1@14dcfad

We define check, a method that takes a DS[String, Any]:

scala> def check(in: DS[String, Any]) = in("333")
check: (DS[String,Any])Any

And we call check with t1:

scala> check(t1)
res14: Any = 333

Invariant Parameter Types

In Scala, Array[T] is invariant. This means that you can only pass an Array[String] to foo(a: Array[String]) and that you can only pass an Array[Object] to bar(a: Array[Object]). Figure 8-4 gives a clear picture of Invariant parameter types.

9781484202333_Fig08-04.jpg

Figure 8-4. Invariance in Scala

This ensures that what is read from or written to the array is something of the correct type. So, for anything that’s mutable, the type parameter should be invariant. You do this by doing nothing with the type parameter. So, let’s define an invariant class (see Listing 8-12).

Listing 8-12. Defining an Invariant Class

class Holder[T](var data: T)

The class holds data of type T. Let’s write a method (see Listing 8-13).

Listing 8-13. add Method

def add(in: Holder[Int]) {in.data = in.data + 1}
scala> def add(in: Holder[Int]) {in.data = in.data + 1}
add: (Holder[Int])Unit
scala> val h = new Holder(0)
h: Holder[Int] = Holder@bc0eba
scala> add(h)
scala> h.data
res2: Int = 1

Because the add method expects an Int to come out of Holder and puts an Int back into the Holder, the type of the Holder must be invariant. That does not mean that invariant containers lose their ability to hold subclasses of their declared type. A Holder[Number] can contain a Double, and an Array[Object] can contain String, Integer, and so on. Let’s put a Double into a Holder[Number]:

scala> val nh = new Holder[Number](33.3d)
nh: Holder[java.lang.Number] = Holder@340c9c

And we define a method that rounds the number:

scala> def round(in: Holder[Number]) {in.data = in.data.intValue}
round: (Holder[java.lang.Number])Unit

We call the round method, and let’s see what we get out the other side:

scala> round(nh)
scala> nh.data
res16: java.lang.Number = 33

We put in a Number and got back a Number. What’s the underlying class for the Number?

scala> nh.data.getClass
res17: java.lang.Class[_] = class java.lang.Integer

Great. Integer is a subclass of Number, so we can put an Integer or a Double into the Holder[Number]. We preserve the ability to use class hierarchies with invariant type parameters. Let’s finally see what happens when we try to pass a Holder[Double] into round.

scala> val dh = new Holder(33.3d)
dh: Holder[Double] = Holder@1801e5f
scala> round(dh)
<console>:8: error: type mismatch;
found : Holder[Double]
required: Holder[java.lang.Number]

So, invariant type parameters protect us when we have mutable data structures such as arrays.

Rules of Variance

So, we’ve successfully defined and used an invariant type. The invariant type was mutable, so it both returned and was called with a particular type. We created a covariant type that was an immutable holder of a value. Finally, we created a transformer that had contravariant input and covariant output. Wait, that sounds like a function. That’s right, Scala’s FunctionN traits have contravariant parameters and covariant results. This leads us to the simple rules of variance:

  • Mutable containers should be invariant.
  • Immutable containers should be covariant.
  • Inputs to transformations should be contravariant, and outputs from transformations should be covariant.

Type Bounds

When defining a parametrized type, bounds allow you to place restrictions on type parameters. Thus a bounded type is restricted to a specific type or its derived type.

Upper Type Bounds

An upper bound type is restricted to a specific type or one of its derived types Scala provides the upper bound relation operator (<:), which you can use to specify an upper bound for a type.

The type parameter A <: AnyRef means any type A that is a subtype of AnyRef. So the <: operator signifies that the type to the left of the <:operator must be a subtype of the type to the right of the <: operator. Moreover the type the left of the <: operator could be the same type of the right of the <: operator.

The type parameter A <: AnyRef means that the type to the left of the <: operator must be derived from the type to the right of the <: operator or the type to the left of the <: operator could be the same type of the right of the <: operator. In other words, the upper type bounds (and as we will explain in the next section, lower type bounds) restrict the allowed types that can be used for a type parameter when instantiating a type from a parameterized type as illustrated in the following Listing 8-15:

Listing 8-15. Defining an Upper Type Bound

def test[A <: AnyRef]

In Listing 8-15, the upper type bound says that any type used for parameter A must be a subtype of AnyRef.

The upper type bound is different from type variance in that type variance determines how actual types of the type are related, for example how the actual types List[AnyRef] and List[String] of the type List are related. Let’s explore this with an example illustrated in Listing 8-16.

Listing 8-16. Defining an Employee Class Hierarchy

class Employee (val name: String)
class Internal (name: String) extends Employee(name)
class FreeLancer(name: String) extends Employee(name)
class Customer (name: String)
scala> class Employee (val name: String)
defined class Employee
scala> class Internal (name: String) extends Employee(name)
defined class Internal
scala> class FreeLancer(name: String) extends Employee(name)
defined class FreeLancer
scala> class Customer (name: String)
defined class Customer

Now define a function that takes a parameter with an upper bound as illustrated in Listing 8-17.

Listing 8-17. Defining a Function that Takes a Parameter with an Upper Bound

def employeeName [A <: Employee](emp: A) { println(emp.name) }
scala> def employeeName [A <: Employee](emp: A) { println(emp.name) }
employeeName: [A <: Employee](emp: A)Unit

Now test the employeeName as shown:

employeeName (new Internal ("Paul"))
scala> employeeName (new Internal ("Paul"))
Paul

Now test with FreeLancer as shown:

employeeName (new FreeLancer ("John"))
scala> employeeName (new FreeLancer ("John"))
John

Now test with the Customer class as shown in Listing 8-18.

Listing 8-18. The Customer Class Is Not a subtype of Employee

employeeName (new Customer ("Peter"))
scala> employeeName (new Customer ("Peter"))
<console>:35: error: inferred type arguments [Customer] do not conform to method
 employeeName's type parameter bounds [A <: Employee]
              employeeName (new Customer ("Peter"))
              ^
<console>:35: error: type mismatch;
 found   : Customer
 required: A
              employeeName (new Customer ("Peter"))

As you can see, because of the upper bound restriction, Listing 8-18 does not compile, as the Customer class is not a subtype of Employee.

Lower Type Bounds

A lower bound type is restricted to a specific type or its supertype. The type selected must be equal to or a supertype of the lower bound restriction. Listing 8-19 defines a lower type bound.

Listing 8-19. Defining Lower Type Bound

class A {
type B >: List[Int]
def someMethod(a : B) = a
}

As you can see, we define type B inside class A to have a lower bound of List[Int]. We instantiate a variable st as a subtype of A as shown in Listing 8-20.

Listing 8-20. Instantiate the Subtype

scala> val st = new A { type B = Traversable[Int] }

We can call the some Method with a Set class. This is because Set, even if not a supertype of the List class, is a subtype of Traversable.

Implicit Class

Using types, especially when type inferencing makes them invisible, is simple and doesn’t take a lot of thought away from the task at hand. Well-defined types and type interactions will stay out of the library consumer’s way but guard against program errors.

We’ve seen a little bit of stuff so far that looks like magic. The String class seems to have grown methods:

scala> "Hello".toList
res0: List[Char] = List(H, e, l, l, o)

You may be wondering how a Java class that is final could have additional methods on it. Well, Scala has a feature called implicit conversion. If you have an instance of a particular type, and you need another type, and there’s an implicit conversion in scope, Scala will call the implicit method to perform the conversion. For example, some date-related methods take Long, and some take java.util.Date. It’s useful to have conversions between the two. We create a method that calculates the number of days based on a Long containing a millisecond count:

scala> def millisToDays(in: Long): Int = (in / (1000L * 3600L * 24L)).toInt

We can calculate the number of days by passing a Long to the method:

scala> millisToDays(5949440999L)
res3: Int = 68

Let’s try to pass a Date into the method:

scala> import java.util.Date
import java.util.Date
scala> millisToDays(new Date)
res31: Int = 16429

But sometimes it’s valuable to convert between one type and another. We are used to the conversion in some contexts: Int image Long, Int image Double, and so on. We can define a method that will automatically be called when we need the conversion:

scala> implicit def dateToLong(d: Date) = d.getTime
dateToLong: (java.util.Date)Long

And this allows us to call millisToDays with a Date instance:

scala> millisToDays(new Date)
res5: Int = 14286

You may think that implicit conversions are dangerous and reduce type safety. In some cases that’s true. You should be very careful with them, and their use should be an explicit design choice. However, we see that sometimes implicit conversions (e.g., Int image Long) are very valuable, for example, when we have a method that takes a parameter that must be a Long:

scala> def m2[T <: Long](in: T): Int = (in / (1000L * 3600L * 24L)).toInt
m2: [T <: Long](T)Int
scala> m2(33)
<console>:8: error: inferred type arguments [Int] do not conform to method m2's
type parameter bounds [T <: Long]
m2(33)
^

So having to type the following could get very old:

scala> m2(33.toLong)
res8: Int = 0

What is the scope of implicits? The Scala compiler considers an implicit in the current scope if:

  • The implicit is defined in the current class or in a superclass.
  • The implicit is defined in a trait or supertrait, or is mixed into the current class or a superclass.
  • The implicit is defined on the companion object of the current target class.
  • The implicit is available on an object that has been imported into the current scope.

When designing libraries, be careful about defining implicits, and make sure they are in as narrow a scope as is reasonable. When consuming libraries, make sure the implicits defined in the objects are narrow enough and are not going to cause problems such as getting stuff from every Option.

Implicit conversions are powerful tools and potentially very dangerous. We mean wicked dangerous. Back in the day, we put the implicit in Listing 8-21 into a library.

Listing 8-21. Implicit Conversion

implicit def oToT[T](in: Option[T]): T = in.get

This was convenient, very convenient. We no longer had to test Options. We just passed them around, and they were converted from an Option to their underlying type. And when we removed the implicit, we had 150 code changes to make. That was 150 latent defects. Using implicits to convert to a class that has a particular method is a good reason. There’s very little likelihood of damage.

scala> implicit def oToT[T](in: Option[T]): T = in.get
warning: there were 1 feature warning(s); re-run with -feature for details
oToT: [T](in: Option[T])T

Until Scala 2.10, implicit conversion was handled by implicit def methods that took the original instance and returned a new instance of the desired type. Implicit methods have been supplanted by implicit classes, which provide a safer and more limited scope for converting existing instances.

Scala 2.10 introduced a new feature called implicit classes. An implicit class is a class marked with the implicit keyword. This keyword makes the class’s primary constructor available for implicit conversions when the class is in scope.

To create an implicit class, simply place the implicit keyword in front of an appropriate class. Here’s an example:

Listing 8-22. Implicit Class

object Helper {
        implicit class Greeting(val x: Int) {
          def greet= "Hello " * x
        }
      }
scala> object Helper {
     |         implicit class Greeting(val x: Int) {
     |           def greet= "Hello " * x
     |         }
     |       }
defined object Helper

To use this class, just import it into scope and call the greet method:

scala> import Helper._
import Helper._
scala> println(3.greet)
Hello Hello Hello

For an implicit class to work, its name must be in scope and unambiguous, like any other implicit value or conversion.

Implicit classes have the following restrictions:

  • They must be defined inside of another trait/class/object.
    object Helpers {
    implicit class RichInt(x: Int) // OK!
    }
    implicit class RichDouble(x: Double) // BAD!
  • They may only take one non-implicit argument in their constructor.
    implicit class RichDate(date: java.util.Date) // OK!
    implicit class Indexer[T](collecton: Seq[T], index: Int) // BAD!
    implicit class Indexer[T](collecton: Seq[T])(implicit index: Index) // OK!

    While it’s possible to create an implicit class with more than one non-implicit argument, such classes aren’t used during implicit lookup.

  • There may not be any method, member or object in scope with the same name as the implicit class. This means an implicit class cannot be a case class.
    object Bar
    implicit class Bar(x: Int) // BAD!
    val x = 5
    implicit class x(y: Int) // BAD!
    implicit case class Baz(x: Int) // BAD!

Abstract Types

The abstract type allows you to define generic classes, but instead of using the conventional syntax, you name them inside the class as shown in Listing 8-23.

Listing 8-23. Abstract Type

trait Container {
  type A
  def value: A
}
scala> trait Container {
     |   type A
     |   def value: A
     | }
defined trait Container

In Listing 8-23, type A is an abstract type member. Now we can implement the value method which returns an Int.

Listing 8-24. Implementing the Method

object OnlyInt extends Container {
  type A = Int

  def value = 2
}

You can also apply constraints to an abstract type member so that the container can only store anything that is of a Number instance. Such constraint can be annotated on a type member right where we defined it first as illustrated in Listing 8-25.

Listing 8-25. Applying Constraint

trait AnyNumber{
  type A <: Number
}

Let’s now mix in the trait AnyNumber as shown in Listing 8-26.

Listing 8-26. Mix in the Trait

trait Container{
  type A <: Number
  def value: A
}

object AnyNumber extends Container {
  def value = 2
}

Higher-Kinded Types

Higher-kinded types use other types to construct a new type. This is similar to higher-order functions that take other functions as parameters. A higher-kinded type can have one or more other types as parameters. In Scala, you can do this using the type keyword.

We will use the :kind command. It allows you to check whether a type is higher kind. Let’s check it out on a simple type constructor, such as List[+A]:

:kind List

scala> :kind List
scala.collection.immutable.List's kind is F[+A]

:kind -v List

scala> :kind -v List
scala.collection.immutable.List's kind is F[+A]
* -(+)-> *
This is a type constructor: a 1st-order-kinded type.

Here we see that scalac can tell us that List, in fact, is a type constructor. Let’s investigate the syntax right above this information:

* -(+)-> *

This says “takes one type, returns another type.”

Something that takes two parameters, say Map[_, _] has the kind:

* -> * -(+)-> *
scala> :kind -v Map[_, _]
scala.collection.immutable.Map's kind is F[A1,+A2]
* -> * -(+)-> *
This is a type constructor: a 1st-order-kinded type.

You can have kinds that are themselves parameterized by higher-kinded types. So, something could not only take a type, but take something that itself takes type parameters.

Summary

In this chapter you learned Scala’s rules about variance, Type bounds, and implicit classes and also a brief overview on Abstract types and higher-kinded types in Scala.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset