Scala type bounds

Versions: Scala 2.12.1

Scala has a rich type system and one of interesting features are type bounds.

This post covers only a small part of Scala's types system. In the first part we'll discover the type bounds, pretty similar with their Java's cousins. The second part will show some test cases illustrating what we can do with presented boundaries.

Type bounds

If you're coming from Java ground, you've certainly met the signatures like:

class MyClass<T extends MyBaseType>
class MyOtherClass<T super MyBaseType>

The first class can be parameterized with types extending MyBaseType while the second with MyBaseType or its subtypes. Scala has similar mechanism. We can use it to either declare upper type bounds or lower type bounds. Both limit the possible type values that can be defined and thus guarantee better type safety.

Concretely an upper type bound is used where the accepted type T must be a subtype of T. It's expressed with T <:. For instance this code shows SoftReference companion:

object SoftReference {
  /** Creates a `SoftReference` pointing to `value` */
  def apply[T <: AnyRef](value: T) = new SoftReference(value)

  /** Optionally returns the referenced value, or `None` if that value no longer exists */
  def unapply[T <: AnyRef](sr: SoftReference[T]): Option[T] = Option(sr.underlying.get)
}

As you can correctly deduce, the methods will accept every instance since AnyRef is a root class for all reference types except Any. Thus, the first line in the following code will compile and the second will not:

// It works since OtherAnyRef extends of AnyRef
class OtherAnyRef extends AnyRef
val softRefOfAnyRefSubtype = SoftReference[OtherAnyRef](new OtherAnyRef())
// It also compiles since the boundary is greater or equal
val softRefOfAnyRef = SoftReference[AnyRef](new AnyRef())
// It doesn't work because AnyVal extends Any and not AnyRef
// The compilation error is given:
// Error:(142, 38) type arguments [com.waitingforcode.types.Test2.OtherAny] do not conform to method apply's type
// parameter bounds [T <: AnyRef]
class OtherAny(val value: Int) extends AnyVal {}
val softRefOtherAny = SoftReference[OtherAny](new OtherAny(2))

The lower type bounds of a type T means that the provided must be a supertype of T. It's expressed with T >: and can also be found in Scala's objects:

// scala.Option.getOrElse
@inline final def getOrElse[B >: A](default: => B): B

class MyClass
class MySubClass extends MyClass
class MyOtherClass

Option[MySubClass](null).getOrElse[MyClass](new MyClass())
// But inverse is not true
// Error:(154, 34) type arguments [com.waitingforcode.types.Test2.MySubClass] do not conform to method getOrElse's
// type parameter bounds [B >: com.waitingforcode.types.Test2.MyClass]
Option[MyClass](null).getOrElse[MySubClass](new MyClass())

Both bounds can be mixed, i.e. we can bound the type from upper and lower side in a single type declaration (e.g. T :> MyClass <: MyOtherClass).

Type bounds examples

The example below shows how the boundaries can be used in different engineering jobs:

class TypeBoundariesSpec extends FunSpec with Matchers {

  describe("frontend application") {
    it("should be coded only by a frontend engineer") {
      val applicationCode = Application.codeFrontend(new JavaScriptFrontendSoftwareEngineer)
      // A frontend application can't be coded by a backend engineer:
      // Error:
      // Error:(24, 19) inferred type arguments [com.waitingforcode.types.ScalaBackendSoftwareEngineer]
      // do not conform to method codeFrontend's type parameter bounds [T <: com.waitingforcode.types.FrontendSoftwareEngineer]
      // Application.codeFrontend(new ScalaBackendSoftwareEngineer)

      applicationCode shouldEqual "<script type=\"text/javascript\">..."
    }
  }
  describe("the JavaScript engineer") {
    it("should have a favorite browser") {
      val favoriteBrowser = Application.getFavoriteBrowser(new ReactJsJavaScriptFontendSoftwareEngineer)

      favoriteBrowser shouldEqual "Chrome"
      // If we try a HTML engineer to work on JavaScript code, it won't compile:
      //Error:(49, 19) inferred type arguments [com.waitingforcode.types.FrontendSoftwareEngineer] do not conform
      // to method codeInJavaScript's type parameter bounds
      // [T >: com.waitingforcode.types.JavaScriptFrontendSoftwareEngineer <:
      // com.waitingforcode.types.JavaScriptFrontendSoftwareEngineer]
      // Application.codeInJavaScript(new HtmlFrontendSoftwareEngineer)
    }
  }
  describe("the check on coding ability") {
    it("should don't allow a PM to code") {
      class ProjectManager() {}
      val projectManager = new ProjectManager()

      val pmCodingAbility = Application.checkIfEngineerCanCode(projectManager)

      pmCodingAbility shouldBe false
    }
    it("should allow Scala engineer to code") {
      val scalaEngineerCodingAbility = Application.checkIfEngineerCanCode(new ScalaBackendSoftwareEngineer)

      scalaEngineerCodingAbility shouldBe true
    }
  }


}
trait SoftwareEngineer {
  def code(): String
}

abstract class BackendSoftwareEngineer extends SoftwareEngineer

class ScalaBackendSoftwareEngineer extends BackendSoftwareEngineer {
  override def code(): String = "package com."
}

abstract class FrontendSoftwareEngineer extends SoftwareEngineer

class JavaScriptFrontendSoftwareEngineer extends FrontendSoftwareEngineer {
  val favoriteBrowser = "Firefox"
  override def code(): String = "<script type=\"text/javascript\">..."
}

class ReactJsJavaScriptFontendSoftwareEngineer extends JavaScriptFrontendSoftwareEngineer {
  override val favoriteBrowser: String = "Chrome"
  override def code(): String = "#react.js"
}

class HtmlFrontendSoftwareEngineer extends FrontendSoftwareEngineer {
  override def code(): String = "<html>"
}

object Application {

  // Upper bound case - accepts FrontendSoftwareEngineer or its subtypes
  def codeFrontend[T <: FrontendSoftwareEngineer](engineer: T): String = {
    engineer.code()
  }

  // Lower bound case - in fact accepts all available objects since they all have the Any class as a source
  def checkIfEngineerCanCode[T >: SoftwareEngineer](engineer: T): Boolean = {
    engineer match {
      case _: SoftwareEngineer => true
      case _ => false
    }
  }

  // Lower and upper bound example - accepts all supertypes of JavaScriptFrontendSoftwareEngineer
  // (FrontendSoftwareEngineer, SoftwareEngineer,....) but also constraints the types to be at least
  // a JavaScriptFrontendSoftwareEngineer instance. Without the last bound the compile can't be sure that the type
  // has the .favoriteBrowser value invoked in the body. It's because one of allowed supertypes is SoftwareEngineer that
  // doesn't expose this property.
  def getFavoriteBrowser[T >: JavaScriptFrontendSoftwareEngineer <: JavaScriptFrontendSoftwareEngineer](engineer: T): String = {
    engineer.favoriteBrowser
  }

}

This short post shown the idea of type boundaries. The first section described upper and lower boundaries that help to limit the accepted types to either subtypes or supertypes. The second part, through an example of web application, presented how these boundaries can be used.


If you liked it, you should read:

📚 Newsletter Get new posts, recommended reading and other exclusive information every week. SPAM free - no 3rd party ads, only the information about waitingforcode!