Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pattern matching on nested types in Scala

I am trying to implement something that is effectively an enumeration in Scala. I would like to do that using case classes so that the compiler is able to detect any non-exhaustive pattern matches.

This works fine in the very basic form, e.g.:

sealed abstract class HorizontalAlignment
case object Left extends HorizontalAlignment
case object Right extends HorizontalAlignment
case object Center extends HorizontalAlignment
case object AsIs extends HorizontalAlignment
...
def test (x : HorizontalAlignment) = 
  x match {
    case Left => ...
    ...  
  } 

However this is not ideal as the names of the case objects can easily clash:

sealed abstract class HorizontalAlignment
case object Left extends HorizontalAlignment
case object Right extends HorizontalAlignment
case object Center extends HorizontalAlignment
case object AsIs extends HorizontalAlignment

sealed abstract class VerticalAlignment
case object Top extends VerticalAlignment
case object Bottom extends VerticalAlignment
case object Center extends VerticalAlignment
case object AsIs extends VerticalAlignment

// "Center" and "AsIs" clash

The obvious solution is to put the case objects into separate namespaces:

sealed abstract class HorizontalAlignment {
  case object Left extends HorizontalAlignment
  case object Right extends HorizontalAlignment
  case object Center extends HorizontalAlignment
  case object AsIs extends HorizontalAlignment
}

sealed abstract class VerticalAlignment {
  case object Top extends VerticalAlignment
  case object Bottom extends VerticalAlignment
  case object Center extends VerticalAlignment
  case object AsIs extends VerticalAlignment
}

But how to reference those classes in a match block?

They cannot be referenced with a Java-style dot:

def test (x : HorizontalAlignment) = 
x match {
  case HorizontalAlignment.Left => 0  //  error: not found: value HorizontalAlignment
}

The "#" symbol also does not seem to work:

def test (x : HorizontalAlignment) = 
x match {
  case HorizontalAlignment#Left => 0 // error: '=>' expected but '#' found 
}

And this form does not work either:

def test (x : HorizontalAlignment) = 
x match {
  case _ : HorizontalAlignment#Left => 0  // error: type Left is not a member of Test.HorizontalAlignment
}

This makes sense as "Left" is in this case an instance and not a type, and I suspect there is an easy way to refer to the type. The closest I could get to achieving that is this:

sealed abstract class HorizontalAlignment {
  case class Left extends HorizontalAlignment
  case class Right extends HorizontalAlignment
  case class Center extends HorizontalAlignment
  case class AsIs extends HorizontalAlignment

  object Left
  object Right
  object Center
  object AsIs

}

But although this makes the match block compile fine I could not find any way to actually refer to those objects, e.g. to pass a member of this "enumeration" to a function. This is because HorizontalAlignment is a type and not an object and therefore it is impossible to refer to one of the nested objects using field access, and, on the other hand, those objects are not types so it is impossible to refer to them using the "#" symbol.

Is there any way to refer to objects nested in a class from outside that class?

EDIT

So far I have found that the package objects are the best way to solve this problem.

package object HorizontalAlignment  {
  sealed abstract class HorizontalAlignment
  case object Left extends HorizontalAlignment
  case object Right extends HorizontalAlignment
  case object Center extends HorizontalAlignment
  case object AsIs extends HorizontalAlignment
}

package object VerticalAlignment {
  sealed abstract class VerticalAlignment 
  case object Top extends VerticalAlignment 
  case object Bottom extends VerticalAlignment 
  case object Center extends VerticalAlignment 
  case object AsIs extends VerticalAlignment 
}


object Test {
  import HorizontalAlignment.HorizontalAlignment
  import VerticalAlignment.VerticalAlignment 

  def test (x : HorizontalAlignment, y : VerticalAlignment) =  {
    x match {
      case HorizontalAlignment.Left => ...
      ...
    }

    y match {
      case VerticalAlignment.Top => ...
      ...
    }
  }

  def testTest = test (HorizongalAlignment.Left, VerticalAlignment.Top)

}

However, the above question (access to nested objects in classes) still stands.

like image 379
Ivan Poliakov Avatar asked Dec 12 '22 13:12

Ivan Poliakov


2 Answers

You don't have to use package objects, which may have some additional undesirable semantics: regular old companion objects are just as good:

sealed trait HorizontalAlignment
object HorizontalAlignment {
  case object Left extends HorizontalAlignment
  case object Right extends HorizontalAlignment
  case object Center extends HorizontalAlignment
  case object AsIs extends HorizontalAlignment
}

scala> def test (x : HorizontalAlignment) = x match {
     |   case HorizontalAlignment.Left => "got left"
     | }

scala> test(HorizontalAlignment.Left)
res0: java.lang.String = got left

The problem you encountered was that since HorizontalAlignment was an abstract class there was no instance of HorizontalAlignment to dereference. With your original namespaced formulation you'd need to instantiate a HorizontalAlignment instance, and the inner objects would be specific to that instance. However, since HorizontalAlignment is sealed, you could not create such an instance in any other compilation unit than the one in which it was defined, so your enumerated values could actually never be obtained by any means.

Unlike Java, there is no "static namespace" associated with classes; to get the equivalent, you have to use a companion object.

like image 112
Kris Nuttycombe Avatar answered Dec 25 '22 22:12

Kris Nuttycombe


You have already been wisely steered away from this structure, but to answer the remaining question: to refer to an a value member of a class for which you hold no instance, you will have to resort to existentials.

sealed abstract class HorizontalAlignment {
  case object Left extends HorizontalAlignment
  case object Right extends HorizontalAlignment
  case object Center extends HorizontalAlignment
  case object AsIs extends HorizontalAlignment
}

object Test {
  type LeftOb = x.Left.type forSome { val x: HorizontalAlignment }

  def test(x: HorizontalAlignment): Int = x match {
    case _: LeftOb => 0
  }
}

Unsurprisingly (well, unsurprisingly if you're me) trying to use that type in a pattern match crashes the bejeezus out of the compiler. But in principle it is the way to express it.

Edit: people seem distracted by my pointing out the pattern matcher crash. Let me illustrate in a less crashy manner that a) this is the only way to express the concept in question and b) it works.

sealed abstract class HorizontalAlignment {
  case object Left extends HorizontalAlignment
  case object Right extends HorizontalAlignment
  case object Center extends HorizontalAlignment
  case object AsIs extends HorizontalAlignment
}

object Test {
  type LeftOb = x.Left.type forSome { val x: HorizontalAlignment }

  def f(x: Any) = x.isInstanceOf[LeftOb]

  def main(args: Array[String]): Unit = {
    val ha = new HorizontalAlignment { }
    println(f(ha.Left))
    println(f(ha.Right)) 
  }
}

Output:

true
false
like image 30
psp Avatar answered Dec 26 '22 00:12

psp