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 TypeTag
s 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?
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?
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)