Case Classes in Scala

April 17, 2018
scala Apprenticeship

In my professional working life, I’ve mostly used Javascript, and some Java as flavor on the plate. Maybe because of my lack of experience or Scala’s more detailed specifications, some features of the language confuses me until I understand why do we need them. After that, I question how did we handle business without that features.

Case classes in conjunction with pattern matching is one of the shiny features. Let me define them first;

trait Flight

case class OneWayFlight(from: String, to: String, departureTime: LocalDateTime) extends Flight
case class RoundTripFlight(from: String, to: String, departureTime: LocalDateTime,
                         returnTime: LocalDateTime) extends Flight
case class TransitFlight(from: String, through: String, to: String, departureTime: LocalDateTime,
                         returnTime: LocalDateTime) extends Flight

With my limited Scala knowledge, I pretend traits as interfaces in Java. Of course, traits are more than that, but it’s a subject for another post. So, we have defined different types of flights by extending Flight trait. Each case class has different construction parameters, so we will need to handle these classes according to their properties.

Assume that, we would like to group vacations by their duration, which is returnTime - depatureTime is not applicable for OneWayFlight, but the Objects we accept are Flight types. How can we implement this? If it was Java, I would write a pseudo control statement as follows;

method calculateDuration(flights) {
  for(flight in Flights) {
    if(flight instanceof OneWayFlight) {
      ...
    } else if(flight instanceof RoundTripFlight || flight instanceof TransitFlight) {
      ...
    }
  }
}

Of course there might be better ways to handle this problem, but in Java, if I add another type of flight, compiler will not remind me to add new type to this code snippet. In Scala, we would write the following;

def aggregateFlights(flights: Seq[Flight]): Seq[Flight] = {
  flights.groupBy {
    case OneWayFlight => None
    case RoundTripFlight => ...
    case TransitFlight => ...
  }
}

So, Scala compiler is aware of the implementations of Flight trait, and by this way, we are able to use pattern matching with our case classes. This might seem like just a syntactic sugar, but it’s way more than that. First of all, a colleague might add a new type of flight without knowing this method. But, in such case, scala compiler raises a warning during compile time, pointing out the unmatched case classes. This is called exhaustiveness check and could really prevent you from bugs.

Another thing to point out, in Scala traits, you have an option to seal trait’s scope to that file. If I change trait Fligth to sealed trait Flight it will not be able to reach outside of the file. But, how could it help me? It keeps your types together and most importantly, it increases the compiler performance. Compiler will check for implementations of the trait, if you seal it, compiler will only check that file, otherwise I assume it will check every tree, but I am not sure about this part.