How to reflect concrete types that corresponds to the type parameters of an abstraction type in Scala?

108 Views Asked by At

Suppose we have a generic type (for example, Seq[E]) and a concrete subtype (for example, Seq[Int]). How can we extract concrete type that corresponds to the type parameters of the abstraction type. In other words, how can we know E -> Int.

Below is a minimal code example that tests for the desired behavior. The extractTypeBinding function would perform the transformation in question.

import scala.reflect.runtime.{universe => ru}

class MyFuncs

object MyFuncs {
  def fn1[E](s: Seq[E]): E = ???
  def fn2[K, V](m: Map[K, V]): Int = ???
}

object Scratch {

  def extractTypeBinding(genType: ru.Type, typeParam: ru.Type)(concreteType: ru.Type): ru.Type = ???

  def getArgTypes(methodSymbol: ru.MethodSymbol): Seq[ru.Type] =
    methodSymbol.paramLists.headOption.getOrElse(Nil).map(_.typeSignature)

  def main(a: Array[String]): Unit = {

    // Grab the argument types of our methods.
    val funcsType = ru.typeOf[MyFuncs].companion
    val fn1ArgTypes = getArgTypes(funcsType.member(ru.TermName("fn1")).asMethod)
    val fn2ArgTypes = getArgTypes(funcsType.member(ru.TermName("fn2")).asMethod)

    val genericSeq = fn1ArgTypes.head  // Seq[E]
    val genericMap = fn2ArgTypes.head  // Map[K, V]

    // Create an extractor for the `E` in `Seq[E]`.
    val seqElExtractor = extractTypeBinding(genericSeq, genericSeq.typeArgs.head) _
    // Extractor for the `K` in `Map[K,V]`
    val mapKeyExtractor = extractTypeBinding(genericMap, genericMap.typeArgs.head) _
    // Extractor for the `V` in `Map[K,V]`
    val mapValueExtractor = extractTypeBinding(genericMap, genericMap.typeArgs(1)) _

    println(seqElExtractor(ru.typeOf[Seq[Int]])) // should be Int
    println(seqElExtractor(ru.typeOf[Seq[Map[String, Double]]])) // should be Map[String, Double]
    println(mapKeyExtractor(ru.typeOf[Map[String, Double]])) // should be String
    println(mapKeyExtractor(ru.typeOf[Map[Int, Boolean]])) // should be Int
    println(mapValueExtractor(ru.typeOf[Map[String, Double]])) // should be Double
    println(mapValueExtractor(ru.typeOf[Map[Int, Boolean]])) // should be Boolean
  }

}

Based on the docstrings, it seems like asSeenFrom should be the key to implementing extractTypeBinding. I tried the below implementation, but it returned the type parameter unchanged.

  def extractTypeBinding(genType: ru.Type, typeParam: ru.Type)(concreteType: ru.Type): ru.Type =
    typeParam.asSeenFrom(concreteType, genType.typeSymbol.asClass)
  
  ...

  println(seqElExtractor(ru.typeOf[Seq[Int]])) // E
  println(seqElExtractor(ru.typeOf[Seq[Map[String, Double]]])) // E

If asSeenFrom is the correct approach, what would the correct incantation be? If not, then how should this be done?

1

There are 1 best solutions below

0
On BEST ANSWER

The simplest solution came from the helpful prodding by Dmytro Mitin in the comments.

I had a couple misunderstandings about .typeArgs that were cleared up with some additional experimentation.

  1. It returns all type arguments, not just the abstract ones.
  2. It only returns the "top level" type arguments of the type you call it on. In other words, Map[A, Map[B, C]] only has 2 type args (A and Map[B, C])

Both of those seem very intuitive now, but I initially made some foolish assumptions. Below is a modified version of my test that more clearly achieves my original intent.

class MyFuncs

object MyFuncs {
  def fn1[E](s: Seq[E]): E = ???
  def fn2[K, V](m: Map[K, V]): Int = ???
}

object Scratch {

  def typeArgBindings(genericType: ru.Type, concreteType: ru.Type): Map[ru.Type, ru.Type] =
    // @todo consider validating both have the same base type.
    genericType.typeArgs.zip(concreteType.typeArgs).toMap

  def getArgTypes(methodSymbol: ru.MethodSymbol): Seq[ru.Type] =
    methodSymbol.paramLists.headOption.getOrElse(Nil).map(_.typeSignature)

  def main(a: Array[String]): Unit = {

    // Grab the argument types of our methods.
    val funcsType = ru.typeOf[MyFuncs].companion
    val fn1ArgTypes = getArgTypes(funcsType.member(ru.TermName("fn1")).asMethod)
    val fn2ArgTypes = getArgTypes(funcsType.member(ru.TermName("fn2")).asMethod)

    val genericSeq = fn1ArgTypes.head  // Seq[E]
    val genericMap = fn2ArgTypes.head  // Map[K, V]

    println(typeArgBindings(genericSeq, ru.typeOf[Seq[Int]]))  // Map(E -> Int)
    println(typeArgBindings(genericSeq, ru.typeOf[Seq[Map[String, Double]]]))  // Map(E -> Map[String,Double])
    println(typeArgBindings(genericMap, ru.typeOf[Map[String, Double]]))  // Map(K -> String, V -> Double)
    println(typeArgBindings(genericMap, ru.typeOf[Map[Int, Boolean]]))  // Map(K -> Int, V -> Boolean)

  }

}