11 April 2016

Path Dependent Types in Scala

by Brendan McAdams

Often confusing to newer Scala developers, “Inner classes” in Scala do not behave the way they do in many other languages. Thus, they can be perceived to be ‘inconsistent’ from expectations in their behavior.

It isn’t possible, given the following class definition:

class Outer {
  class Inner
  def put(inner: Inner): Unit = ()

To instantiate the Inner class directly:

scala> new Outer.Inner
<console>:12: error: not found: value Outer
       new Outer.Inner

Instead, we need an instance of Outer to create an Inner instance:

val outer1 = new Outer
val inner1 = new outer1.Inner
val outer2 = new Outer
val inner2 = new outer2.Inner

In other words, the path to an Inner instance depends upon the Outer instance. Note as well that there is no true relationship between outer1.Inner and outer2.Inner:

scala> outer1.put(inner1)

scala> outer1.put(inner2)
<console>:16: error: type mismatch;
 found   : outer2.Inner
 required: outer1.Inner

The put method on outer1 will not accept just any arbitrary instance of Inner: It must receive one derived from its own instance. ”Great“, you may say, ”but where is this useful?”. In the end, a powerful use of Path Dependent types is creating a tighter coupling between two related classes, in which the compiler can help us avoid mistakes.

Imagine, if you will, a (relatively simple) model of Airplane, Seat, and Passenger:

case class Passenger(firstName: String, lastName: String, middleInitial: Option[Char])

class Airplane(flightNumber: Long) {
  case class Seat(row: Int, seat: Char)
  def seatPassenger(passenger: Passenger, seat: Seat): Unit = ()

Given two separate Airplanes, it should be impossible to accidentally sit on the wrong plane!

scala> val flt102 = new Airplane(102)
flt102: Airplane = Airplane@4b8d604b

scala> val flt506 = new Airplane(506)
flt506: Airplane = Airplane@4dc27487

scala> val kelland = Passenger("Mike", "Kelland", None)
kelland: Passenger = Passenger(Mike,Kelland,None)

scala> val mcadams = Passenger("Brendan", "McAdams", Some('W'))
mcadams: Passenger = Passenger(Brendan,McAdams,Some(W))

scala> val nash = Passenger("Michael", "Nash", None)
nash: Passenger = Passenger(Michael,Nash,None)

scala> val flt102Seat1A = new flt102.Seat(1, 'A')
flt102Seat1A: flt102.Seat = Seat(1,A)

scala> val flt506Seat21A = new flt506.Seat(21, 'A')
flt506Seat21A: flt506.Seat = Seat(21,A)

scala> val flt506Seat20F = new flt506.Seat(20, 'F')
flt506Seat20F: flt506.Seat = Seat(20,F)

If three members of the BoldRadius team are headed on journeys, but only two are boarding the same flight… what if we are so wrapped up in conversation that one of us tries boarding the wrong plane?

scala> flt506.seatPassenger(nash, flt506Seat20F)

scala> flt506.seatPassenger(mcadams, flt506Seat21A)

scala> flt506.seatPassenger(kelland, flt102Seat1A)
<console>:19: error: type mismatch;
 found   : flt102.Seat
 required: flt506.Seat
       flt506.seatPassenger(kelland, flt102Seat1A)

Excellent! The compiler catches that while Michael Nash and myself are headed out on the same flight, Mike Kelland isn’t. When Mike tries to board the wrong flight… the type system prevents it.

In short, we can use the Path Dependent Type logic to prevent crossing over into runtime bugs – if we’re smart about our domain design.