0
votes

Let X be a type constructor with type parameters A1, A2, ..., An. For example Option[A] and Function1[A1, A2].

Let X[T1, T2, ..., Tn] be the type resulting of applying the type constructor X to the concrete type argument T1, T2, ... Tn. For example Option[Int] and Function1[Long, List[String]].

Let Y be a direct subclass of X that leaves some of the type parameters of X unfixed, does not add new free type parameter, and its type parameters are B1, B2, ..., Bm with m <= n. For example Some[B] and PartialFunction[B1, B2].

I need to implement a function that finds out the concrete types R1, R2, ..., Rm to be assigned to the type parameters B1, B2, ..., Bm of the type constructor Y such that Y[R1, R2, ..., Rm] <:< X[T1, T2, ..., Tn] with all the variances removed (all traits and classes considered non variant).

In the case of Option and Some it is clear that R1 = T1 for Some[R1] <:< Option[T1] with variance removed be true. Also, in the case of Function1 and PartialFunction is clear that R1 = T1 and R2 = T2 for PartialFunction[R1, R2] <:< Function[T1, T2] with variance removed be true. But for the general case is somewhat more complicated.

Given that both, the compiler and the <:< operator of the reflect library, have to solve this problem in order to check assignability; I assume that a function of the reflection API already solves my problem. But I don't find it. Y suppose it is def asSeenFrom(pre: Type, clazz: Symbol): Type, but I tried it with no avail. Surely, I am doing something wrong.

This is an usage example of the function that answers this question:

import scala.reflect.runtime.universe._

val optionOfInt: Type = typeOf[Option[Int]]
val someTypeConstructor: ClassSymbol = typeOf[Some[_]].typeSymbol.asClass
val someOfInt: Type = instantiateSubclassTypeConstructor(optionOfInt, someTypeConstructor)

print(someOfInt.typeArgs) // outputs "List(Int)"

Where instantiateSubclassTypeConstructor is the function which answers this question.

/** @param baseInstantiation a class constructor instantiation. For example: {{{typeOf[Option[Int]]}}}
* @param directSubclassSymbol the [[ClassSymbol] of the class constructor we want to instantiate such that it is assignable to `baseInstantiantion`. For example: {{{typeOf[Some[_]].typeSymbol.asClass}}}
* @return a type instantiation of the class constructor referenced by the `directSubclassSymbol` such that it is assignable to `baseInstantiantion`. For example: {{{typeOf[Some[Int]]}}}
*/
def instantiateSubclassTypeConstructor(baseInstantiation: Type, directSubclassSymbol: ClassSymbol): Type = ???

scala version: 2.13.3

1
it is clear that R1 = T1 for Some[R1] <:< Option[T1] is that right though? I think that Some[R1] <:< Option[T1] iff R1 <:< T1. And are you aware of type erasure? I believe, what you're trying to solve can't be solved for the general case. Only for some very specific cases where type definitions preserved at compile time. - SimY4
@SimY4 What I said is correct because I clarified "with variance removed". It think it can be solved. If I am right the compiler needs to solve it for type inference, and also the "<:<" operator in some way. Also, if I remember well, I solved it for java many years ago and I think I can solve it by hand for scala too. But I suppose that it is already solved in the reflect API and I am not able to find it. - Readren
@SimY4 Please look the doc of the method def asSeenFrom(pre: Type, clazz: Symbol): Type defined in scala.reflect.api.Types. Apparently it solves this problem. But I am unable to understand how to use it. The example in the doc is obsolete because it uses ThisType which was removed from the API. - Readren
Side note: I am sure there is a better way to express "is assignable with variance removed", by I lack the terminology. Perhaps the question would be more clear if I replace that with "corresponds to". - Readren
@Readren Use internal.thisType(C) instead of ThisType(C). - Dmytro Mitin

1 Answers

0
votes

This is a partial answer. It only finds out the relationship between the type arguments of the base class X and the type parameters of the direct subclass Y.

import scala.reflect.runtime.universe._

/** @param baseType a type resulting of the instantiation of a type constructor. For example: {{{typeOf[Option[Int]]}}}
 * @param directSubclassTypeConstructor the type constructor whose type parameters we want to instantiate. It should be a subclass of the `baseType`'s type constructor.
 * @return the relationship between `baseType`'s type arguments and `directSubclassTypeconstructor`'s type parameters. For example: {{{Map(A -> Int)}}}*/
def typeParametersToBaseTypeArgumentsRelationship(baseType: Type, directSubclassTypeConstructor: Type): Map[Type, Type] = {
    val baseTypeConstructor = baseType.typeConstructor;
    assert(directSubclassTypeConstructor <:< baseTypeConstructor)

    val typeParamsRelationship =
        for {
            (baseTypeParam, baseTypeArgument) <- baseTypeConstructor.typeParams zip baseType.typeArgs
        } yield {
            val directSubclassTypeParam = baseTypeParam.asType.toType.asSeenFrom(directSubclassTypeConstructor, baseType.typeSymbol)
            directSubclassTypeParam -> baseTypeArgument
        }
    typeParamsRelationship.toMap
}

An usage example:

scala> import scala.reflect.runtime.universe._
scala> typeParametersToBaseTypeArgumentsRelationship(
    typeOf[Function1[Long, List[String]]],
    typeOf[PartialFunction[_, _]].typeConstructor
)
val res1: Map[reflect.runtime.universe.Type, reflect.runtime.universe.Type] =
    Map(A -> Long, B -> List[String])

Another more intrincate usage example:

sealed trait X[A1, A2, A3]
class Y[B1, B2] extends X[B2, List[B1], B1] {}

scala> typeParametersToBaseTypeArgumentsRelationship(
    typeOf[X[Long, List[String], String]],
    typeOf[Y[_, _]].typeConstructor
)
val res2: Map[reflect.runtime.universe.Type,reflect.runtime.universe.Type] =
    Map(B2 -> Long, List[B1] -> List[String], B1 -> String)

The missing part to respond the question is the creation of a copy of directSubclassTypeConstructor with the type parameters instantiated according to the given relationship. That requires a knowledge that I don't have.

Edit: Having found out how to apply type arguments to a type constructor I am able to complete this answer.

import scala.reflect.runtime.universe._

/** Given a type `baseType` and a type constructor of one of its direct subclasses `directSubclassTypeConstructor`, creates a type by applying said type constructor to the type arguments that were used to create the `baseType` as seen from said direct subclass.
 * @param baseType a type resulting of the instantiation of a type constructor. For example: {{{typeOf[Option[Int]]}}}
 * @param directSubclassTypeConstructor the type constructor we want to instantiate such that it is assignable to `baseType`. For example: {{{typeOf[Some[_]].typeConstructor}}}
 * @return the type constructed by applying the type constructor `directSubclassTypeConstructor` to the type arguments of `baseType` as seen from said type constructor. For example: {{{typeOf[Some[Int]]}}}*/
def applySubclassTypeConstructor(baseType: Type, directSubclassTypeConstructor: Type): Type = {
    val directSubclassTypeParams = directSubclassTypeConstructor.typeParams
    if( directSubclassTypeParams.isEmpty) {
        directSubclassTypeConstructor
    } else {
        val baseTypeConstructor = baseType.typeConstructor;
        assert(directSubclassTypeConstructor <:< baseTypeConstructor)

        val subclassTypeParamsToBaseTypeArgumentsRelationship=
            for {
                (baseTypeParam, baseTypeArgument) <- baseTypeConstructor.typeParams zip baseType.typeArgs
            } yield {
                val directSubclassTypeParam = baseTypeParam.asType.toType.asSeenFrom(directSubclassTypeConstructor, baseType.typeSymbol)
                directSubclassTypeParam -> baseTypeArgument
            }

        val directSubclassTypeArguments =
            for (subclassTypeParm <- directSubclassTypeParams) yield {
                subclassTypeParamsToBaseTypeArgumentsRelationship.find { r =>
                    r._1.typeSymbol.name == subclassTypeParm.name
                }.get._2
            }

        appliedType(directSubclassTypeConstructor, directSubclassTypeArguments)
    }
}

A simple usage example:

scala> applySubclassTypeConstructor(
    typeOf[Option[Int]],
    typeOf[Some[_]].typeConstructor
)
val res1: reflect.runtime.universe.Type =
     Some[Int]

An intrincate usage example:

sealed trait X[A1, A2, A3]
class Y[B1, B2] extends X[B2, List[B1], B1] {}

scala> applySubclassTypeConstructor(
    typeOf[X[Long, List[String], String]],
    typeOf[Y[_, _]].typeConstructor
)
val res2: reflect.runtime.universe.Type =
    Y[String,Long]