Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Replacing case class inheritance with extractors preserving exhaustiveness checks in Scala

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.

like image 977
Ivan Poliakov Avatar asked Jun 13 '11 16:06

Ivan Poliakov


2 Answers

In general, this can't be done.

Sealed classes are a special case (no pun intended) because scalac knows at compile time how many matches are possible.

But since extractors allow you to run arbitrary code and because of the damnable halting problem, there's no way for the compiler to guarantee in every case that you'll check every case. Consider:

def test(foo: Int) {
  foo match {
    case IsMultipleOf8(n) => printf("%d was odd\n", n)
  }
}

This is not exhaustive because it doesn't handle numbers that aren't multiples of 8, but the compiler cannot deduce (without running your extractor on all Int's) that only some values of type Int are multiples of 8.

like image 107
Bill Avatar answered Nov 12 '22 19:11

Bill


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.

like image 33
KIMA Avatar answered Nov 12 '22 18:11

KIMA