Question

I have a container for heterogeneous types

trait Elem

trait Obj {
  type E <: Elem
  def elem: E
}

trait Foo extends Elem

There should not be any sub-classes of Obj. I am looking for suggestions as how to elegantly match an instance of Obj against a number of element types to gain a refined Obj.

For example:

def withFooObj(obj: Obj { type E = Foo }) = ()

def test(obj: Obj): Unit = obj.elem match {
  case f: Foo =>
    withFooObj(obj.asInstanceOf[Obj { type E = Foo }]) // !
  case _ =>
}

What would be an elegant idea to get rid of the cast? Ideally it would work as a pattern match.


For example, I can create individual extractors:

object IsFoo {
  def unapply(obj: Obj): Option[Obj { type E = Foo }] =
    if (obj.elem.isInstanceOf[Foo]) Some(obj.asInstanceOf[Obj { type E = Foo }])
    else None
}

def test(obj: Obj): Unit = obj match {
  case IsFoo(x) => withFooObj(x)
  case _ =>
}

But I have many sub-types of Elem and would prefer to have a generic extractor.


Edit: Since this seems to be a tough one, I want to add another relaxation: It is allowed to change Obj to contain a type parameter instead of a type member (if it helps). It is not allowed to require sub-classes of Obj.

Was it helpful?

Solution

I would expect this to be impossible. Consider (assuming class Foo extends Elem)

val obj1 = new Obj {
  type E = Elem
  def elem = new Foo
}

Since obj.elem does match f: Foo, but obj1 doesn't actually have type Obj { type E = Foo }, you do have to cast. This cast is actually safe in this case, but only because Obj doesn't have e.g. any methods taking E. Actually, it isn't safe, since the value of the first call to elem having class Foo doesn't mean all calls to elem will return Foo.

EDIT: if you are all right with merely hiding the cast in the extractor, you may be able to do something like this:

case class IsElem[T <: Elem]()(implicit ct: ClassTag[T]) {
  def unapply(obj: Obj): Option[Obj { type E = T }] =
    if (ct.runtimeClass.isInstance(obj.elem)) Some(obj.asInstanceOf[Obj { type E = T }])
      else None
}

val IsFoo = IsElem[Foo]
val IsBar = IsElem[Bar]
...

EDIT2: with type parameters, you can do

(obj, obj.elem) match {
  case (obj: Obj[Foo] @unchecked, f: Foo) => ...
  case (obj: Obj[Bar] @unchecked, f: Bar) => ...
}

Obviously, this doesn't help safety issues.

OTHER TIPS

(Here I wanted to be clever:)

trait Idea {
  def resolve[A](obj: Obj)
      (f: Function1[(Obj { type E = E1 }, E1) forSome { type E1 <: Elem }, A]): A
}

def test(obj: Obj, idea: Idea): Unit = idea.resolve(obj) {
  case (x, _: Foo) => withFooObj(x)
  case _ =>
}

But Scalac doesn't think that E1 == Foo here...

I'm going to focus on your requirement for a heterogeneous container, and ignore certain details like the type member, and pattern matching.

Summary

The goal is to have some way to process an Obj based on its elem type. The important parts of the working solution:

  • Make Obj an abstract class taking a type parameter representing elem.
  • Automatically associate a Processor parameterized on the same type as elem, by using an implicit parameter.
  • Define implicit Processors for all the Elem types you want to use.

I consider defining the implicit Processors as minimal boilerplate (if you can actually call it boilerplate). You need to defined elem-specific processors anyway, so these implicit objects are a good place to organize them.

The code:

trait Elem {
  // for debugging only
  def typeName: String
}

abstract class Obj[E <: Elem](implicit val processor: Processor[E]) {
  def elem: E

  def process(): Unit = processor.process(this)
}

trait Processor[R <: Elem] {
  def process(obj: Obj[R]): Unit
}
object Processor {
  implicit val fooProcessor = new Processor[Foo] {
    def process(obj: Obj[Foo]): Unit = println(s"Processing Foo: ${obj.elem.typeName}")
  }
  implicit val barProcessor = new Processor[Bar] {
    def process(obj: Obj[Bar]): Unit = println(s"Processing Bar: ${obj.elem.typeName}")
  }
  implicit val widgetProcessor = new Processor[Widget] {
    def process(obj: Obj[Widget]): Unit = println(s"Processing Widget: ${obj.elem.typeName}")
  }
}

trait Foo extends Elem { def typeName = "Foo" }
trait Bar extends Elem { def typeName = "Bar" }
trait Widget extends Elem { def typeName = "Widget" }

def test2[R <: Elem](obj: Obj[R]): Unit = obj.process()

// MAIN

val objFoo = new Obj[Foo] {
  def elem: Foo = new Foo { }
}
println("===== test2 on a static-typed Foo")
test2(objFoo)

val objBar = new Obj[Bar] {
  def elem: Bar = new Bar { }
}
println("===== test2 on a static-typed Bar")
test2(objBar)

val objWidget = new Obj[Widget] {
  def elem: Widget = new Widget { }
}
println("===== test2 on a static-typed Widget")
test2(objWidget)

println("===== test2 on a heterogeneous list of `Obj`s")
val heteroList = List(objFoo, objBar, objWidget)
heteroList.foreach(test2(_))

Output:

===== test2 on a static-typed Foo
Processing Foo: Foo
===== test2 on a static-typed Bar
Processing Bar: Bar
===== test2 on a static-typed Widget
Processing Widget: Widget
===== test2 on a heterogeneous list of `Obj`s
Processing Foo: Foo
Processing Bar: Bar
Processing Widget: Widget
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top