Higher kinded types

Our example of the glass has become a bit boring. To make it fascinating again, we'll add another abstraction, a jar. This is how our model will look after that:

sealed trait Contents
case class Water(purity: Int) extends Contents
case class Whiskey(label: String) extends Contents
sealed trait Container[C <: Contents] { def contents: C }
case class Glass[C<: Contents](contents: C) extends Container[C]
case class Jar[C <: Contents](contents: C) extends Container[C]

The glass and the jar can both be filled with any contents. For instance, this is how it can be done:

def fillGlass[C <: Contents](c: C): Glass[C] = Glass(c)
def fillJar[C <: Contents](c: C): Jar[C] = Jar(c)

As we can see, both methods look identical with respect to the type used to construct the result. The parameterized type that is used to construct types is called a type constructor. As a consistent language, Scala allows you to abstract over type constructors in the same way it allows you to abstract over functions via higher order functions (more about this in the next chapter). This abstraction over type constructors is called higher kinded types. The syntax requires us to use an underscore to denote the expected type parameter on the definition side. The implementation should then use the type constructor without type constraints.

We can use a type constructor to provide a generic filling functionality. Of course, we can't get rid of the specific knowledge about how to fill our containers, but we can move it to the type level:

sealed trait Filler[CC[_]] {
def fill[C](c: C): CC[C]
}
object GlassFiller extends Filler[Glass] {
override def fill[C](c: C): Glass[C] = Glass(c)
}
object JarFiller extends Filler[Jar] {
override def fill[C](c: C): Jar[C] = Jar(c)
}

In the preceding code, we're using the type constructor CC[_] to denote both Glass and Jar in the Filler trait. We can now use created abstractions to define a generic filling functionality:

def fill[C, G[_]](c: C)(F: Filler[G]): G[C] = F.fill(c)

The G[_] type is a type constructor for glass and jar, and Filler[G] is a higher order type that uses this type constructor to build a full G[C] for any content, C. This is how the generic fill method can be used in practice:

val fullGlass: Glass[Int] = fill(100)(GlassFiller)
val fullJar: Jar[Int] = fill(200)(JarFiller)

This might not look like a huge win over the specific methods for now because we've provided our type constructors explicitly. The real advantage will become obvious the moment we start talking about implicits in Chapter 4, Getting to know Implicits and Type Classes.

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

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