Ben Kovitz
Ben Kovitz

Reputation: 5020

Extending from trait where each instance's singleton type is type parameter

Suppose I want every instance of an Item to be a SourceOf that Item, and every SourceOf a subtype of Item to be a SourceOf all instances of that subtype. This seems like a natural way to do it:

object Demo {
  trait Item extends SourceOf[this.type]

  trait SourceOf[+A <: Item]
}

Obviously this won't work, since this refers to the Demo object, not to each Item instance. (I've tested it.)

How can I tell the Scala compiler that every Item is a source of itself? It should be possible to get the compiler to deduce stuff like:

Here's an example (written to ignore type erasure for brevity):

trait FarmingImplement extends Item
object TheRototiller extends FarmingImplement

def findSource(item: Item, seq: Seq[SourceOf[_]]): Option[SourceOf[item.type]] =
  seq.collectFirst {
    case src: SourceOf[item.type] => src
  }

val sources = Seq(   // example: possible sources of TheRototiller
  new SourceOf[Nothing] { override def toString = "Wrong source" },
  new SourceOf[FarmingImplement] { override def toString = "Right" },
  TheRototiller /* also right */ )

val got = findSource(TheRototiller, sources).get
println(got)  // Should print "Right", since the second source in `sources`
              // is the first match.

I want the type of got be SourceOf[TheRototiller.type], not SourceOf[Item]. But mainly I want Scala's type system to do the work of determining whether a SourceOf matches a given Item or category of Items.

Upvotes: 2

Views: 163

Answers (1)

dk14
dk14

Reputation: 22374

At least, this gonna solve your problem with passing this:

object Demo {

  trait C[+A]

  trait Item extends SourceOf {
    type T = C[this.type]
  }

  trait SourceOf {
    type T <: C[Item]
  }
}

Example (you'll have to compare Ts instead of SourceOfs here):

scala> val i = new Item{}
i: Demo.Item = $anon$1@c70cb4c

scala> implicitly[i.T =:= i.T] //compile-time type equality check
res4: =:=[i.T,i.T] = <function1>

scala> val i2 = new Item{}
i2: Demo.Item = $anon$1@1f26ac06

scala> implicitly[i.T =:= i2.T]
<console>:18: error: Cannot prove that i.T =:= i2.T.
              implicitly[i.T =:= i2.T]

If you need to check types in runtime (as you've shown in example with pattern matching) - I would recomend to use TypeTag:

import scala.reflect.runtime.universe._

object Demo {

  abstract class C[+A: TypeTag] 

  trait Item extends SourceOf {
    type T = C[this.type]
  }

  trait SourceOf {
    val tag = typeTag[this.type] //just to access
  }
}

Usage:

scala> val i = new Item{}
i: Demo.Item = $anon$1@3b2111b0

scala> val i2 = new Item{}
i2: Demo.Item = $anon$1@4e7bb48a

scala> typeTag[i.T].tpe =:= typeTag[i2.T].tpe //runtime type-equality check
res9: Boolean = false

scala> typeTag[i.T].tpe =:= typeTag[i.T].tpe
res10: Boolean = true

Finally, implementation of findSource:

import scala.reflect.runtime.universe._

object Demo {

      trait Item extends SourceOf 

      trait SourceOf {
        type I = this.type
        val tag = typeTag[I]
      }

      def findSource(item: Item, seq: Seq[SourceOf]): Option[item.I] =
        seq.find(_.tag.tpe =:= item.tag.tpe).asInstanceOf[Option[item.I]]
}

Examples:

scala> val i = new Item{}
i: Demo.Item = $anon$1@1fc2899d

scala> val i2 = new Item{}
i2: Demo.Item = $anon$1@5f308abb

scala> val ri = findSource(i, Seq(i, i2)).get
ri: i.I = $anon$1@1fc2899d

scala> implicitly[ri.I =:= i.I]
res2: =:=[ri.I,i.I] = <function1>

asInstanceOf is used (it's safe to do here) because Seq looses path-dependent type in compile-time and we used runtime-check to match types, so no way to find it for compiler. However, Shapeless2's HList + Selector based implementation might be more pure.

Upvotes: 1

Related Questions