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: