Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala Case class matching compile error with aliased inner types?

Tags:

types

match

scala

How do I use case class matching with aliased types? This works when I pull CB etc out of Container.

class DoStuff[TKey](
  val c : Container[TKey]#CB
)
{
  type CB = Container[TKey]#CB
  type C1 = Container[TKey]#C1
  type C2 = Container[TKey]#C2

  c match {
    case C1(e1) => e1  //   - not found: value e1   - not found: value C1
    case C2(e2) => e2 //    - not found: value e2   - not found: value C2
  }
}

trait Container[TKey]
{
    abstract trait CB
    case class C1(val e : AnyRef) extends CB
    case class C2(val e : AnyRef) extends CB
}

Thanks!

like image 856
ACyclic Avatar asked Feb 28 '23 00:02

ACyclic


1 Answers

Right... Inner classes in Scala are a bit fiddly. Let's try a simple example before I show you the rewritten version of the code you have provided.

case class Foo(x: Int) {
  case class Bar(y: String) 
}

Now, consider the following code snippet:

val x = new Foo(1)
val y = new Foo(2)

val a = new x.Bar("one")
val b = new y.Bar("two") 

The most generic type of a and b is Foo#Bar, which means the inner class Bar with any outer object of type Foo. But we could be more specific in saying that the type of a is x.Bar and the type of b is y.Bar - which means that a is an instance of the inner class Bar with the outer object x, similar for b.

You can actually see that the types are different by calling typeOf(a) and typeOf(b), where typeOf is a utility method defined as such. (it just gives the type of its argument by quite nice type inference and a bit of use of Manifests)

def typeOf[T](x: T)(implicit m: scala.reflect.Manifest[T]) = m.toString

As an inner object holds a reference to its enclosing object, you cannot instantiate an inner object without somehow specifying its outer object. Therefore, you can call new x.Bar("one") but you cannot call new Foo#Bar("?") - as in the second case you haven't specified what is the inner object for the new object you try to construct.

So, let's return to your code snippet. When you are pattern matching, you are actually calling a constructor - when calling C1(e1). As C1 is an alias for Container[TKey]#C1 you have tried to call a constructor of an inner class without specifying its outer object, which fails due to the reasons outlined above. The way I would write the code would be as follows:

trait Container[TKey] {
    abstract trait CB
    case class C1(val e : AnyRef) extends CB
    case class C2(val e : AnyRef) extends CB
}

class DoStuff[TKey] (val c: Container[TKey], val element: Container[TKey]#CB) {
  element match {
    case c.C1(e1) => Some(e1)
    case c.C2(e2) => Some(e2)
    case _        => None
  }
}

Now this compiles and hopefully it does what you want. But take this with great care! Due to type erasure, Scala cannot guarantee that the element is actually of type c.CB or of type d.CB where the CB in the case of c and d happen to be the same.

Consider this example:

def matcher(arg: Foo#Bar) = {
  arg match {
    case x.Bar(n) => println("x");
    case y.Bar(n) => println("y");
  }
}

where x and y are as before. Try running the following:

matcher(a)
matcher(b) 

They both print x!

Therefore I would rewrite the code to explicitly have an element in the container:

trait Container[TKey] {
    abstract trait CB
    case class C1(val e : AnyRef) extends CB
    case class C2(val e : AnyRef) extends CB
    val element: CB
}

class DoStuff[TKey](val c: Container[TKey]) {
  c.element match {
    case c.C1(e1) => Some(e1)
    case c.C2(e2) => Some(e2)
    case _        => None
  }
}

Hope it helps :)

-- Flaviu Cipcigan

like image 77
Flaviu Cipcigan Avatar answered May 08 '23 23:05

Flaviu Cipcigan