In Scala 2.13, why is it possible to summon unqualified TypeTag for abstract type?

86 Views Asked by At

Considering the following code:

import scala.reflect.api.Universe

object UnqualifiedTypeTag {

  val RuntimeUniverse = scala.reflect.runtime.universe

  trait HasUniverse {

    val universe: Universe with Singleton

    def uType: RuntimeUniverse.TypeTag[universe.type] = implicitly
  }

  object HasRuntime extends HasUniverse {

    override val universe: RuntimeUniverse.type = RuntimeUniverse
  }

  def main(args: Array[String]): Unit = {
    println(HasRuntime.uType)
  }
}

Ideally, this part of the program should yield TypeTag[HasRuntime.universe.type], or at least fail its compilation, due to implicitly only able to see universe.type, which is not known at call site. (in contrast, WeakTypeTag[universe.type] should totally work)

Surprisingly, the above program yields TypeTag[HasUniverse.this.universe.type]. This apparently breaks many contracts, namely:

  • TypeTag cannot be initialised from abstract types, unlike WeakTypeTag
  • TypeTag can always be erased to a Class

What's the purpose of this design, and what contract does TypeTag provide? In addition, is this the reason why ClassTag was supposed to be superseded after Scala 2.11, but instead was kept as-is until now?

1

There are 1 best solutions below

5
Dmytro Mitin On

You seem to still confuse implicitly[X] and (implicit x: X)

When doing implicit resolution with type parameters, why does val placement matter?

In scala 2, can macro or any language feature be used to rewrite the abstract type reification mechanism in all subclasses? How about scala 3?

SYB `cast` function in Scala

Setting abstract type based on typeclass

There is no implicit resolution in the line println(HasRuntime.uType). Implicits are resolved in the right hand side of the line def uType: RuntimeUniverse.TypeTag[universe.type] = implicitly. For implicits, the call site is not HasRuntime.uType, the call site is implicitly.

Simpler example is the following (let's make the code self-contained and remove macros or compiler TypeTag magic, keeping ordinary implicits)

trait MyTypeclass[A] {
  def apply(): String
}
object MyTypeclass {
  implicit def universe[U <: Universe]: MyTypeclass[U] = () => "Universe"
  implicit val runtimeUniverse: MyTypeclass[RuntimeUniverse.type] = () => "RuntimeUniverse"
}

trait Universe
object RuntimeUniverse extends Universe

trait HasUniverse {
  val universe: Universe with Singleton
  def uType: MyTypeclass[universe.type] = implicitly
}

object HasRuntime extends HasUniverse {
  override val universe: RuntimeUniverse.type = RuntimeUniverse
}

def main(args: Array[String]): Unit = {
  println(HasRuntime.uType()) // Universe (not RuntimeUniverse)
}

Even simpler example:

trait MyTypeclass[A] {
  def apply(): String
}
object MyTypeclass {
  implicit def parent[A <: Parent]: MyTypeclass[A] = () => "Parent"
  implicit val child: MyTypeclass[Child.type] = () => "Child"
}

trait Parent {
  type T <: Parent
  def uType: MyTypeclass[T] = implicitly
}

object Child extends Parent {
  override type T = Child.type
}

def main(args: Array[String]): Unit = {
  println(Child.uType()) // Parent (not Child)
}

If you want implicits to resolve in the line println(Child.uType) then add an implicit parameter to the method

trait MyTypeclass[A] {
  def apply(): String
}
object MyTypeclass {
  implicit def parent[A <: Parent]: MyTypeclass[A] = () => "Parent"
  implicit val child: MyTypeclass[Child.type] = () => "Child"
}

trait Parent {
  type T <: Parent
  def uType(implicit mtc: MyTypeclass[T]): MyTypeclass[T] = implicitly
     // the same as
  // def uType(implicit mtc: MyTypeclass[T]): MyTypeclass[T] = mtc
}

object Child extends Parent {
  override type T = Child.type
}

def main(args: Array[String]): Unit = {
  println(Child.uType.apply()) // Child
}
trait MyTypeclass[A] {
  def apply(): String
}
object MyTypeclass {
  implicit def universe[U <: Universe]: MyTypeclass[U] = () => "Universe"
  implicit val runtimeUniverse: MyTypeclass[RuntimeUniverse.type] = () => "RuntimeUniverse"
}

trait Universe
object RuntimeUniverse extends Universe

trait HasUniverse {
  val universe: Universe with Singleton

  def uType(implicit mtc: MyTypeclass[universe.type]): MyTypeclass[universe.type] = implicitly
     // the same as
  // def uType(implicit mtc: MyTypeclass[universe.type]): MyTypeclass[universe.type] = mtc
}

object HasRuntime extends HasUniverse {
  override val universe: RuntimeUniverse.type = RuntimeUniverse
}

def main(args: Array[String]): Unit = {
  println(HasRuntime.uType.apply()) // RuntimeUniverse
}
import scala.reflect.api.Universe

val RuntimeUniverse = scala.reflect.runtime.universe

trait HasUniverse {
  val universe: Universe with Singleton

  def uType(implicit ttag: RuntimeUniverse.TypeTag[universe.type]): RuntimeUniverse.TypeTag[universe.type] = implicitly
     // the same as
  // def uType(implicit ttag: RuntimeUniverse.TypeTag[universe.type]): RuntimeUniverse.TypeTag[universe.type] = ttag
}

object HasRuntime extends HasUniverse {
  override val universe: RuntimeUniverse.type = RuntimeUniverse
}

def main(args: Array[String]): Unit = {
  println(HasRuntime.uType) // TypeTag[HasRuntime.universe.type]
}

One more option to postpone implicit resolution besides adding an implicit parameter is inlining (with inline in Scala 3 or a macro in Scala 2)

Why the Scala compiler can provide implicit outside of object, but cannot inside? (answer)

Type parameter for implicit valued method in Scala - Circe

Implicit Json Formatter for value classes in Scala

due to implicitly only able to see universe.type, which is not known at call site. (in contrast, WeakTypeTag[universe.type] should totally work)

Here you seem to have one more confusion. The type universe.type is not abstract, it's a singleton type inhabited by the val universe of trait HasUniverse. It's val universe that is abstract but not its type.

Abstract value types are introduced by type parameters and abstract type bindings.

https://scala-lang.org/files/archive/spec/2.13/03-types.html