2

I can get the formal type parameters (type variables) of any Java class/interface, including those defined in Scala, through java.lang.reflect API.

In addition, Scala allows to define type parameters of its classes as contravariant or covariant, information which is not present in JLS. The conventional wisdom is thus that one needs to use Scala 2 TypeTags to access this information, as Java language reflection does not have provisions for this eventuality.

Lack of the latter in Scala 3 leads me to seek alternatives.

The Scala compiler has to work even for compiled classes in absence of their source code, meaning that information has to be present in the byte code in some form.

If true, how can it be accessed, presumably with the help of some byte code manipulation library?

4
  • Maybe github.com/zio/izumi-reflect ? Commented Nov 2 at 11:51
  • 1. Last time I checked, there was no variance information, but that was way before it was even donated to ZIO. I see there still isn't much in the way of documentation, and by casual browsing of the code I can't find anything relating to variance. Commented 2 days ago
  • 2. It doesn't answer the question of how scalac does it, either. While whatever izumi provides will certainly prove useful in the future, it doesn't solve my current predicament. Commented 2 days ago
  • There is this github.com/zio/izumi-reflect/blob/…, - it suggest that typeTag.ref match { case LightTypeTagRef.FullReference(_, params, _) => param.map(_.variance) } should work. Ad 2, as @som-snytt mentioned: ScalaSig is used by Scala 2. Scala 3 uses it to read 2.13 bytecode , but does not emit it. When 2.13 reads Scala 3 bytecode it needs -Ytasty-reader to get information from TASTY. So either use izymi-reflect, or a macro, or the tasty inspector. Commented yesterday

1 Answer 1

5

If you are looking for a solution specific to Scala 3, variance is stored in the TASTy format (.tasty files), which is part of the compiled artifact alongside .class files.

Here is a brief example to show how to use it (I couldn't get to get it working on Scastie, but it should be trivial to run with a recent distribution of Scala):

//> using scala 3.7.3
//> using dep org.scala-lang:scala3-tasty-inspector_3:3.7.3

class Box[+A]
class Sink[-A]
class Invar[A]

import scala.tasty.inspector.*
import scala.quoted.*

object ShowVariance extends Inspector:
  def inspect(using Quotes)(tastys: List[Tasty[quotes.type]]): Unit =
    import quotes.reflect.*
    val wanted = Set("Box", "Sink", "Invar")
    for tasty <- tastys do
      val root = tasty.ast
      new TreeTraverser:
        override def traverseTree(tree: Tree)(owner: Symbol): Unit =
          tree match
            case cd: ClassDef if wanted(cd.symbol.fullName) =>
              val rendered = cd.symbol.typeMembers
                .filter(_.isTypeParam)
                .map { p =>
                  val v =
                    if p.flags.is(Flags.Covariant) then "+"
                    else if p.flags.is(Flags.Contravariant) then "-"
                    else "0"
                  s"${p.name} -> $v"
                }.mkString(", ")
              println(s"${cd.symbol.fullName}: $rendered")
            case _ => super.traverseTree(tree)(owner)
      .traverseTree(root)(Symbol.spliceOwner)

@main def run(): Unit =
  val loader = Thread.currentThread.getContextClassLoader
  val tastyNames = List("Box.tasty", "Sink.tasty", "Invar.tasty")
  val paths =
    tastyNames
      .flatMap(n => Option(loader.getResource(n)))
      .map(u => java.nio.file.Paths.get(u.toURI).toString)

  TastyInspector.inspectTastyFiles(paths)(ShowVariance)

This should print the following

Box: A -> +
Sink: A -> -
Invar: A -> 0
Sign up to request clarification or add additional context in comments.

4 Comments

I am aware of TASTy, although I haven't used it personally yet, so I am very grateful for the example. I hoped however for something cross-version; after all, Scala 2 doesn't have TASTy, and - barring binary incompatibilities due to evolving standard library - can be run by Scala 3 programs. So the variance information has to also be stored in class byte code somehow, I presume.
The ClassfileParser for Scala 2 and 3 each consumes a ScalaSig attribute with Scala 2 symbol info. (I see Scala 3 lets you turn it off with -Yscala2-unpickler.) Scala 2 is not modular; I assume it needs most of the compiler to use reflection, although conceptually it's just loading symbols into the symbol table. It looks like both versions need to operate at typer phase, for example.
I have not understood a word, but it looks like the answer I was looking for. At least I have some keywords to lookup, thanks. Is it somehow accessible from the Java level, or do I have to parse the bytecode of the class in order to find the attribute?
Technically, I guess to separate solutions for Scala 3 and Scala 2 would be acceptable if fully source compatible (and ideally binary compatible).

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.