Scala Type Bounds

Type bounds restrict what types a generic type parameter can accept. Instead of saying "A can be any type," you say "A must be a subtype of Animal" or "A must be a supertype of Dog." This lets you write generic code that relies on specific capabilities without losing flexibility.

Upper Bound — A must be a subtype of B

class Animal(val name: String):
  def breathe(): Unit = println(s"$name breathes")

class Dog(name: String) extends Animal(name):
  def bark(): Unit = println(s"$name barks")

class Cat(name: String) extends Animal(name):
  def meow(): Unit = println(s"$name meows")

// [A <: Animal] means A must be Animal or any subtype
def makeNoise[A <: Animal](animals: List[A]): Unit =
  animals.foreach(a => println(a.name))

makeNoise(List(new Dog("Rex"), new Dog("Buddy")))    // works
makeNoise(List(new Cat("Luna")))                     // works
// makeNoise(List("not an animal"))  // Error: String is not <: Animal

[A <: Animal]
       │
       └── <: means "is a subtype of" (upper bound)

Animal             ← bound (ceiling)
├── Dog            ← allowed
└── Cat            ← allowed
String             ← NOT allowed

Upper Bound for Comparison

// Ordered[A] provides comparison capability
def findMax[A <: Ordered[A]](items: List[A]): Option[A] =
  items match
    case Nil  => None
    case list => Some(list.reduce((a, b) => if a >= b then a else b))

// Use with Comparable types
println(findMax(List(3, 1, 9, 4, 7)))              // Some(9)
println(findMax(List("banana", "apple", "mango"))) // Some(mango)
println(findMax(List.empty[Int]))                  // None

Lower Bound — A must be a supertype of B

// [A >: Dog] means A can be Dog or any supertype of Dog
def wrap[A >: Dog](items: List[A]): List[A] = items

val dogs: List[Dog] = List(new Dog("Rex"))
val animals: List[Animal] = wrap(dogs)   // A becomes Animal — supertype of Dog

[A >: Dog]
       │
       └── >: means "is a supertype of" (lower bound)

Any                ← allowed
AnyRef             ← allowed
Animal             ← allowed (supertype of Dog)
Dog                ← allowed (itself)
Puppy extends Dog  ← NOT allowed (subtype of Dog)

Context Bound — A must have an implicit instance

// [A : Ordering] means: there must be an Ordering[A] in scope
def sortList[A : Ordering](items: List[A]): List[A] =
  items.sorted

println(sortList(List(5, 2, 8, 1, 9)))              // List(1, 2, 5, 8, 9)
println(sortList(List("banana", "apple", "cherry"))) // List(apple, banana, cherry)

[A : Ordering]  is shorthand for  [A](using ord: Ordering[A])

Scala auto-provides Ordering for Int, String, Double, etc.

Combining Bounds

// Upper bound AND context bound together
def processAnimals[A <: Animal : Manifest](items: List[A]): Unit =
  println(s"Processing ${items.length} ${implicitly[Manifest[A]].runtimeClass.getSimpleName}s")
  items.foreach(a => println(s"  ${a.name}"))

processAnimals(List(new Dog("Rex"), new Dog("Buddy")))
// Processing 2 Dogs
//   Rex
//   Buddy

View Bound (Legacy) → Use implicit parameter instead

// Modern Scala: use implicit parameter or context bound
def printAll[A](items: List[A])(using show: A => String): Unit =
  items.map(show).foreach(println)

given intToString: (Int => String) = _.toString
printAll(List(1, 2, 3))

Practical: Bounded Numeric Operations

def statistics[A](data: List[A])(using num: Numeric[A]): Map[String, A] =
  Map(
    "sum" -> data.reduce(num.plus),
    "min" -> data.reduce(num.min),
    "max" -> data.reduce(num.max)
  )

val intStats = statistics(List(10, 5, 20, 15, 8))
println(intStats)   // Map(sum -> 58, min -> 5, max -> 20)

val dblStats = statistics(List(1.5, 3.2, 0.8, 2.7))
println(dblStats)   // Map(sum -> 8.2, min -> 0.8, max -> 3.2)

Type Bound Summary


Bound           Syntax    Meaning
──────────────  ────────  ─────────────────────────────────────────
Upper bound     A <: B    A must be B or a subtype of B
Lower bound     A >: B    A must be B or a supertype of B
Context bound   A : TC    An implicit TC[A] must be available

Type bounds are the bridge between generic code and specific capabilities. They let you write functions that work across many types while still using type-specific operations like comparison, ordering, or arithmetic — all checked at compile time.

Leave a Comment

Your email address will not be published. Required fields are marked *