Scala type bounds - <: vs <:<
In this short entry I want to shed some light at the difference between <:
and <:<
in Scala.
TL;DR
<:
is a language feature built into the compiler: upper type bound<:<
is it’s generalization: generalized type constraints - it’s provided in the standard library
Let’s start with the first one. <:
is called the upper type bound.
We use it to express type boundaries for generic types as for example here:
//> using scala "2.13"
sealed trait SampleTrait
case class SampleValue(v: String) extends SampleTrait
object SampleSingleton extends SampleTrait
def f[X <: SampleTrait](x: X): Unit =
println(x)
f(SampleSingleton)
f(SampleValue("hello"))
case class C(v: String)
// f(C("hello")) // Doesn't compile
That sounds sensible and useful when you need to narrow down the types your method/class is defined for.
If you accidentally add an extra <
after that, there’s an operator that’s imported by default. It’s defined as
sealed abstract class <:<[-From, +To] extends (From => To)
Meaning you can use it like this A <:< SampleTrait
.
This is called the generalized type constraint. Let’s see how we can use it for data type we defined above:
def g[X](x: X)(implicit ev: X <:< SampleTrait): Unit =
println(x)
g(SampleSingleton)
g(SampleValue("hello"))
// g(C("hello")) // Doesn't compile
So what’s the point with having those two anyway?
There’s an use case where you can take advantage. It’s the case when you generalize whole class/trait with the type A
and then you only need to add limitations to specific methods like this:
trait Algebra[A] {
def op(a: A, b: A): A
}
implicit val algebraForA: Algebra[SampleTrait] = ??? // irrelevant
implicit class Test[A](private val a: A) {
def worksForAnyType(): Unit = println("worksForAnyType")
def worksForSampleTraitSubtype(b: A)(implicit ev: A <:< SampleTrait): Unit = println(
s"worksForSampleTraitSubtype ${implicitly[Algebra[SampleTrait]].op(a, b)}"
)
}
10.worksForAnyType() // returns worksForAnyType
// 10.worksForSampleTraitSubtype(10) this doesn't compile
SampleValue("hello").worksForAnyType() // returns worksForAnyType
SampleValue("hello").worksForSampleTraitSubtype(SampleValue("world")) // returns worksForSampleTraitSubtype SampleValue("hello world")
It’s typically used by libraries rather than business code, you can find similar usages in standard library e.g. IterableOnce.toMap
.
Here’s the gist with the full example if you want to give it a try: https://gist.github.com/majk-p/75fe4466cb9c2d315da22341175f0747
Useful links:
- https://github.com/scala/scala/blob/v2.13.8/src/library/scala/collection/IterableOnce.scala#L190
- https://github.com/scala/scala/blob/v2.13.4/src/library/scala/typeConstraints.scala#L146
- https://www.scala-lang.org/api/2.13.4/scala/$less$colon$less.html
- https://www.scala-lang.org/api/2.13.4/scala/$eq$colon$eq.html
- https://docs.scala-lang.org/tour/upper-type-bounds.html
- https://gist.github.com/retronym/229163
- https://debasishg.blogspot.com/2010/08/using-generalized-type-constraints-how.html