scalaconstraintsinlinecompile-timescala-3

Is there a way to constraint a type parameter to accept only types that are specific (rejects abstract type parameters)?


The following code compiles with no warning but does not work as intended.

inline def isXAssignableToY[X <: Matchable, Y]: Boolean = {
    inline erasedValue[X] match {
        case _: Y => true
        case _ => false
    }
}

def isSerializable[A <: Matchable]: Boolean = isXAssignableToY[A, Serializable]

def isSuperOfString[B]: Boolean = isXAssignableToY[String, B]

@main def test19058(): Unit = {
    val a = isXAssignableToY[String, Serializable]
    println(a) // prints true as intended

    val b = isSerializable[String]
    println(b) // prints false, not as intended

    val c = isSuperOfString[Serializable]
    println(c) // prints false, not as intended
}

The methods isSerializable and isSuperOfString don't do what is intended because they pass an abstract type parameter to isXAssignableToY.

How can I constraint the isXAssignableToY method usage such that the compiler complains when the type arguments are abstract type parameters?

I know that the problem is solved adding the inline modifier to the methods. But that is not what I am asking.


I tried using two no-op summonInline of the TypeTags like this:

    inline def isXAssignableToY[X <: Matchable, Y]: Boolean = {
        summonInline[ClassTag[X]]
        summonInline[ClassTag[Y]]
        inline erasedValue[X] match {
            case _: Y => true
            case _ => false
        }
    }

Which is better because it checks that the arguments passed to X and Y aren't abstract type parameters. But that is not a complete solution because it is not checking if the abstract type parameter is nested as a type parameter of a type constructor.


To solve the nested cases I tried with a type class that generates implicit instances recursively:

    trait IsSpecific[T]

    object IsSpecific {
        inline given [F[_], A](using fs: IsSpecific[A], ct: ClassTag[F[A]]): IsSpecific[F[A]] = new IsSpecific[F[A]] {}

        inline given [T](using ct: ClassTag[T]): IsSpecific[T] = new IsSpecific[T] {}

        // Add more cases as needed for different type constructors
    }

    inline def isXAssignableToY[X <: Matchable, Y](using xs: IsSpecific[X], ys: IsSpecific[Y]): Boolean = {
        inline erasedValue[X] match {
            case _: Y => true
            case _ => false
        }
    }

But for this to work the first given must have priority over the second, and I don't know how to achieve that.

And even if it worked, it is excessively verbose if all type constructor arities are considered.


This question is related to this other question: Misleading unreachable warning when a type pattern match that is the result of a inline expansion is involved?


Solution

  • Why not to implement the logic on type level?

    type IsXAssignableToY[X, Y] <: Boolean = X match
      case Y => true
      case _ => false
    
    inline def isXAssignableToY[X, Y]: IsXAssignableToY[X, Y] =
      constValue[IsXAssignableToY[X, Y]]
    
    inline def isSerializable[A]: IsXAssignableToY[A, Serializable] = isXAssignableToY[A, Serializable]
    
    inline def isSuperOfString[B]: IsXAssignableToY[String, B] = isXAssignableToY[String, B]
    

    or

    transparent inline def isSerializable[A]: Boolean = isXAssignableToY[A, Serializable]
    
    transparent inline def isSuperOfString[B]: Boolean = isXAssignableToY[String, B]
    

    How can I constraint the isXAssignableToY method usage such that the compiler complains when the type arguments are abstract type parameters?

    I know that the problem is solved adding the inline modifier to the methods. But that is not what I am asking.

    Checking whether a type parameter is abstract upon a method call sounds like a macro stuff. But defining a macro you anyway will use inline. So why not to use inline in the first place?

    And be careful, there can always be some type synonym (so maybe types will have to be dealiased)

    type SomeAbstractType = SomeConcreteType
    

    Type class IsAbstract (Scala 3): Type negation in Scala 3

    IsTrait (Scala 2): How to require at compile time that a type parameter be a trait (and not a class or other type value)?

    But that is not a complete solution because it is not checking if the abstract type parameter is nested as a type parameter of a type constructor.

    Once when I needed something similar I defined a type class DeepClassTag (Scala 2)

    Why is the spark.implicits._ import not helping with encoder derivation inside a method?

    https://github.com/apache/spark/pull/38740/files#diff-56f71e373de90aa6de7d4c3a2054dd6192bf86f529502aced07fdb3a398d657dR39

    But for this to work the first given must have priority over the second, and I don't know how to achieve that.

    Why do you think that they are prioritized incorrectly? The return type IsSpecific[F[A]] is more specific than the return type IsSpecific[T] and more specific implicits have higher priority. Scala spec:

    If there are several eligible arguments which match the implicit parameter's type, a most specific one will be chosen using the rules of static overloading resolution.

    https://scala-lang.org/files/archive/spec/3.4/07-implicits.html#implicit-parameters

    trait IsSpecific[T]
    
    object IsSpecific {
      inline given recur[F[_], A](using fs: IsSpecific[A], ct: ClassTag[F[A]]): IsSpecific[F[A]] = new IsSpecific[F[A]] {}
      inline given base[T](using ct: ClassTag[T]): IsSpecific[T] = new IsSpecific[T] {}
    }
    
    // switch on: scalacOptions ++= Seq("-Vprint:typer", "-Xprint-types")
    summon[IsSpecific[Int]] // base
    summon[IsSpecific[Option[Int]]] // recur
    

    But be careful. Are you aware that the following compiles in Scala 3 (on contrary to Scala 2)?

    type Foo[_]
    summon[ClassTag[Foo[Int]]] // compiles
    

    https://scastie.scala-lang.org/DmytroMitin/HfedCKCUQoewUTdsMYzelg/6 (Scala 3)

    https://scastie.scala-lang.org/DmytroMitin/HfedCKCUQoewUTdsMYzelg/8 (Scala 2)