Replacing case class inheritance with extractors preserving exhaustiveness checks in Scala

Ivan Poliakov picture Ivan Poliakov · Jun 13, 2011 · Viewed 7.7k times · Source

I have a simple class hierarchy that represents a graph-like structure with several distinct types of vertexes implemented using case classes:

sealed trait Node

sealed abstract case class Vertex extends Node
case class Arc extends Node

case class VertexType1 (val a:Int) extends Vertex
case class VertexType2 (val b:Int) extends Vertex

This allows me to write match blocks like this:

def test (x: Node) = x match {
  case _ : Arc => "got arc"
  case _ : Vertex => "got vertex"
}

or like this:

def test (x: Node) = x match {
  case _ : Arc => "got arc"
  case c : Vertex => c match {
    case _ : VertexType1(a) => "got type 1 vertex " + a
    case _ : VertexType2(a) => "got type 2 vertex " + a
  }
}

Note that this implementation has the following properties:

1) It allows writing match blocks that differentiate between arcs and vertices, but not between specific vertex types, but also match blocks that do differentiate between vertex types.

2) In both vertex-type-specific and non-vertex-type-specific match blocks the exhaustiveness of pattern matching is checked.

However, inheritance from case classes is deprecated, and the compiler suggests to use extractors instead to support matching on non-leaf nodes (i.e., in the above example, to differentiate between arcs and vertices, but not between vertex types).

The question: is it possible to implement a similar class hierarchy without using case class inheritance, but still having pattern exhaustiveness checks performed by the compiler in both use cases shown above?

EDIT: I have added a constructor parameter to the VertexType classes so that the match is not performed only on types.

My current implementation without the case classes is as follows:

sealed trait Node

sealed abstract class Vertex extends Node
class Arc extends Node

class VertexType1 (val a:Int) extends Vertex
class VertexType2 (val b:Int) extends Vertex

object VertexType1 {
  def unapply (x : VertexType1) : Some[Int] = Some(x.a)
}

object VertexType2 {
  def unapply (x : VertexType2) : Some[Int] = Some(x.b)
}

And the test code:

def test (x: Node) = x match {
  case _ : Arc => "got arc" 
  case v : Vertex => v match {
    case VertexType1(a) => "got vertex type 1 " + a 
  }
}

I expect a warning about non-exhaustive match in the second block (VertexType2 is never matched), but there isn't one.

Actually, Scala compilers before 2.9.0-RC3 produce a warning that I expect to see, but versions starting with RC3 (including 2.9.0 and 2.9.0-1) do not, which is rather confusing.

Answer

KIMA picture KIMA · Jun 13, 2011

extractors give you the possibility to use it in pattern matching like case classes in scala, but they don´t have other standard implementations you get when using the case modifier. But hust these extra implementation(especially the implementation of equals) is what makes case class inheritance dangerous and so it got deprecated.

However sealed classes are an orthogonal feature and you can use them whether or not you have a case class or an extractor. By using extractors you don´t get standard implementations on the fly but thus you can have inheritance of extractors - they are simply normal classes with an unapply and/or unapplySeq method.

What You´ve done in your example is called pattern-matching on types. If you only want to do this, you don´t need case classes neither extractors. You can do it with every class you want. So you can simply remove the case modifier.

So to conclude: exhaustiveness of pattern is achieved by sealed class hirachies. Pattern matching is achieved by extractors and case classes of what the latter is just an extractor with appealing standard implementations of frequently used functions. I hope this helped.