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.
