Как отразить конкретные типы, которые соответствуют параметрам типа абстракции в Scala?

Предположим, у нас есть универсальный тип (например, Seq[E]) и конкретный подтип (например, Seq[Int]). Как мы можем извлечь конкретный тип, который соответствует параметрам типа абстракции. Другими словами, откуда мы можем знать E -> Int.

Ниже приведен минимальный пример кода, который проверяет желаемое поведение. Функция extractTypeBinding выполнит рассматриваемое преобразование.

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
  }

}

Судя по строкам документации, кажется, что asSeenFrom должен быть ключом к реализации extractTypeBinding. Я попробовал приведенную ниже реализацию, но она вернула параметр типа без изменений.

  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

Если asSeenFrom правильный подход, то каким будет правильное заклинание? Если нет, то как это нужно сделать?


person Erp12    schedule 07.12.2020    source источник
comment
Вы не слишком усложняете extractTypeBinding? Что не так с concreteType.typeArgs.head?   -  person Dmytro Mitin    schedule 07.12.2020
comment
И concreteType.typeArgs.tail.head для параметра 2-го типа Map.   -  person Dmytro Mitin    schedule 07.12.2020


Ответы (1)


Самое простое решение пришло из полезных советов Дмытро Митина в комментариях.

У меня было несколько недоразумений по поводу .typeArgs, которые были прояснены дополнительными экспериментами.

  1. Он возвращает все аргументы типа, а не только абстрактные.
  2. Он возвращает только аргументы типа верхнего уровня того типа, для которого вы его вызываете. Другими словами, Map[A, Map[B, C]] имеет только 2 аргумента типа (A и Map[B, C]).

Оба они сейчас кажутся очень интуитивными, но изначально я сделал несколько глупых предположений. Ниже приведена модифицированная версия моего теста, которая более четко соответствует моему первоначальному замыслу.

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)

  }

}
person Erp12    schedule 07.12.2020