Advanced Types: Variance, Type Bounds, and Higher-Kinded Types
Introduction
Scala's type system is one of its most powerful features, offering sophisticated tools for building flexible and type-safe code. Advanced type features like variance, type bounds, and higher-kinded types enable you to create generic libraries that are both safe and expressive.
This lesson will teach you to leverage these advanced type system features to build robust APIs, understand complex library designs, and create your own type-safe abstractions.
Understanding Variance
Covariance and Contravariance
// Basic variance concepts
class Animal { def name: String = "Animal" }
class Dog extends Animal { override def name: String = "Dog" }
class Cat extends Animal { override def name: String = "Cat" }
// Invariant container (default)
class InvariantBox[T](val value: T)
val dogBox: InvariantBox[Dog] = new InvariantBox(new Dog)
// val animalBox: InvariantBox[Animal] = dogBox // Compile error!
// Covariant container (+T means T can be "read out")
class CovariantBox[+T](val value: T)
val covariantDogBox: CovariantBox[Dog] = new CovariantBox(new Dog)
val covariantAnimalBox: CovariantBox[Animal] = covariantDogBox // OK!
// You can read from covariant containers
def readFromBox[T](box: CovariantBox[T]): T = box.value
val animal: Animal = readFromBox(covariantDogBox)
println(s"Read from box: ${animal.name}")
// Contravariant container (-T means T can be "written into")
trait ContravariantProcessor[-T] {
def process(item: T): String
}
class AnimalProcessor extends ContravariantProcessor[Animal] {
def process(animal: Animal): String = s"Processing ${animal.name}"
}
class DogProcessor extends ContravariantProcessor[Dog] {
def process(dog: Dog): String = s"Processing dog: ${dog.name}"
}
val animalProcessor: ContravariantProcessor[Animal] = new AnimalProcessor
val dogProcessor: ContravariantProcessor[Dog] = animalProcessor // OK!
val dog = new Dog
println(dogProcessor.process(dog))
// Function variance (contravariant in input, covariant in output)
val animalToString: Animal => String = _.name
val dogToString: Dog => String = animalToString // OK! Function[-T, +R]
val stringToAnimal: String => Animal = name => new Animal { override def name = name }
val stringToDog: String => Dog = stringToAnimal // Compile error!
// Practical variance example: List
val dogs: List[Dog] = List(new Dog, new Dog)
val animals: List[Animal] = dogs // List is covariant
// Producer/Consumer pattern with variance
trait Producer[+T] {
def produce(): T
}
trait Consumer[-T] {
def consume(item: T): Unit
}
class AnimalProducer extends Producer[Animal] {
def produce(): Animal = new Animal
}
class DogConsumer extends Consumer[Dog] {
def consume(dog: Dog): Unit = println(s"Consuming dog: ${dog.name}")
}
// Variance in action
val dogProducer: Producer[Dog] = new AnimalProducer // Compile error! Need covariance
val animalConsumer: Consumer[Animal] = new DogConsumer // OK! Contravariance
// Safe variance example
trait SafeContainer[+T] {
def get: T
// def put(item: T): Unit // Would cause compile error in covariant position
}
trait SafeSink[-T] {
def put(item: T): Unit
// def get: T // Would cause compile error in contravariant position
}
// Variance with methods
class VariantMethods[+T] {
// Covariant type can appear in return position
def getValue: T = ???
// Contravariant position requires lower bound
def setValue[U >: T](value: U): VariantMethods[U] = new VariantMethods[U]
// Using contravariant function parameter
def map[U](f: T => U): VariantMethods[U] = new VariantMethods[U]
// Variance with higher-order functions
def filter(predicate: T => Boolean): VariantMethods[T] = new VariantMethods[T]
}
val variantStringContainer = new VariantMethods[String]
val variantAnyContainer: VariantMethods[Any] = variantStringContainer.setValue(42)
// Mutable collections and invariance
import scala.collection.mutable
class Buffer[T] {
private val items = mutable.ArrayBuffer[T]()
def add(item: T): Unit = items += item
def get(index: Int): T = items(index)
def size: Int = items.size
}
val dogBuffer = new Buffer[Dog]
// val animalBuffer: Buffer[Animal] = dogBuffer // Invariant - compile error!
// This prevents unsafe operations:
// animalBuffer.add(new Cat) // Would break type safety if allowed
// Variance bounds in practice
trait Repository[+T] {
def findById(id: String): Option[T]
def findAll(): List[T]
}
trait MutableRepository[T] extends Repository[T] {
def save[U >: T](entity: U): U
def delete(id: String): Boolean
}
class UserRepository extends MutableRepository[User] {
private var users = Map.empty[String, User]
def findById(id: String): Option[User] = users.get(id)
def findAll(): List[User] = users.values.toList
def save[U >: User](entity: U): U = {
// Implementation would save the entity
entity
}
def delete(id: String): Boolean = {
users.get(id) match {
case Some(_) => users -= id; true
case None => false
}
}
}
case class User(id: String, name: String)
case class AdminUser(id: String, name: String, permissions: Set[String]) extends User(id, name)
val userRepo = new UserRepository
val repo: Repository[User] = userRepo // Covariance allows this
// Lower bounds allow saving supertypes
val admin = AdminUser("1", "Admin", Set("read", "write"))
userRepo.save(admin) // Works due to lower bound [U >: User]
Advanced Variance Patterns
// Variance in type constructors
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
// Option is covariant
implicit val optionFunctor = new Functor[Option] {
def map[A, B](fa: Option[A])(f: A => B): Option[B] = fa.map(f)
}
// List is covariant
implicit val listFunctor = new Functor[List] {
def map[A, B](fa: List[A])(f: A => B): List[B] = fa.map(f)
}
// Function type variance
// Function1[-T, +R] is contravariant in input, covariant in output
def processWithFunction[A, B](items: List[A], f: A => B): List[B] = items.map(f)
val numbers = List(1, 2, 3, 4, 5)
val toString: Any => String = _.toString
val results = processWithFunction(numbers, toString) // Works due to contravariance
// Variance with existential types
trait Container {
type ContentType
def content: ContentType
}
class StringContainer(val content: String) extends Container {
type ContentType = String
}
class IntContainer(val content: Int) extends Container {
type ContentType = Int
}
// Working with existential types
def processContainer(container: Container): String = {
container.content.toString // We can only use methods available on Any
}
// Variance in practice: Event system
trait Event
case class UserEvent(userId: String, action: String) extends Event
case class SystemEvent(level: String, message: String) extends Event
trait EventHandler[-E <: Event] {
def handle(event: E): Unit
}
class UserEventHandler extends EventHandler[UserEvent] {
def handle(event: UserEvent): Unit =
println(s"Handling user event: ${event.userId} -> ${event.action}")
}
class GeneralEventHandler extends EventHandler[Event] {
def handle(event: Event): Unit =
println(s"Handling general event: $event")
}
// Event system with contravariance
class EventSystem {
private var handlers = Map.empty[Class[_], List[EventHandler[Event]]]
def registerHandler[E <: Event](eventType: Class[E], handler: EventHandler[E]): Unit = {
// Contravariance allows us to accept more specific handlers
val currentHandlers = handlers.getOrElse(eventType, List.empty)
handlers += eventType -> (handler.asInstanceOf[EventHandler[Event]] :: currentHandlers)
}
def publish[E <: Event](event: E): Unit = {
val eventType = event.getClass
handlers.getOrElse(eventType, List.empty).foreach(_.handle(event))
}
}
val eventSystem = new EventSystem
val userHandler = new UserEventHandler
val generalHandler = new GeneralEventHandler
eventSystem.registerHandler(classOf[UserEvent], userHandler)
eventSystem.registerHandler(classOf[Event], generalHandler)
eventSystem.publish(UserEvent("user123", "login"))
eventSystem.publish(SystemEvent("INFO", "System started"))
// Variance with phantom types
trait FileFormat
trait CSV extends FileFormat
trait JSON extends FileFormat
trait XML extends FileFormat
class DataProcessor[+F <: FileFormat] private (data: String) {
def process(): String = s"Processing $data"
}
object DataProcessor {
def csv(data: String): DataProcessor[CSV] = new DataProcessor[CSV](data)
def json(data: String): DataProcessor[JSON] = new DataProcessor[JSON](data)
def xml(data: String): DataProcessor[XML] = new DataProcessor[XML](data)
}
// Covariance allows treating specific formats as general formats
val csvProcessor: DataProcessor[CSV] = DataProcessor.csv("csv data")
val fileProcessor: DataProcessor[FileFormat] = csvProcessor // Covariance
def processAnyFormat(processor: DataProcessor[FileFormat]): String =
processor.process()
println(processAnyFormat(csvProcessor))
// Complex variance example: Serialization
trait Serializer[-T] {
def serialize(value: T): String
}
trait Deserializer[+T] {
def deserialize(data: String): T
}
trait Codec[T] extends Serializer[T] with Deserializer[T]
class StringCodec extends Codec[String] {
def serialize(value: String): String = s"\"$value\""
def deserialize(data: String): String = data.stripPrefix("\"").stripSuffix("\"")
}
class AnyCodec extends Codec[Any] {
def serialize(value: Any): String = value.toString
def deserialize(data: String): Any = data
}
// Variance allows flexible codec usage
val stringSerializer: Serializer[String] = new StringCodec
val anySerializer: Serializer[Any] = stringSerializer // Contravariance
val stringDeserializer: Deserializer[String] = new StringCodec
val anyDeserializer: Deserializer[Any] = stringDeserializer // Covariance
println(stringSerializer.serialize("hello"))
println(anySerializer.serialize(42)) // Works due to contravariance
Type Bounds
Upper and Lower Bounds
// Upper bounds (T <: UpperBound)
trait Drawable {
def draw(): String
}
class Circle extends Drawable {
def draw(): String = "Drawing a circle"
def radius: Double = 5.0
}
class Rectangle extends Drawable {
def draw(): String = "Drawing a rectangle"
def area: Double = 10.0
}
// Upper bound ensures T is a subtype of Drawable
class Canvas[T <: Drawable] {
private var shapes = List.empty[T]
def add(shape: T): Unit = shapes = shape :: shapes
def drawAll(): String = shapes.map(_.draw()).mkString(", ")
def count: Int = shapes.length
}
val circleCanvas = new Canvas[Circle]
circleCanvas.add(new Circle)
println(circleCanvas.drawAll())
// Lower bounds (T >: LowerBound)
trait Container[+T] {
def get: T
// Lower bound allows adding supertypes
def add[U >: T](item: U): Container[U]
}
class SimpleContainer[+T](private val item: T) extends Container[T] {
def get: T = item
def add[U >: T](newItem: U): Container[U] = new SimpleContainer(newItem)
}
val dogContainer: Container[Dog] = new SimpleContainer(new Dog)
val animalContainer: Container[Animal] = dogContainer.add(new Cat) // Lower bound allows this
// Context bounds (syntactic sugar for implicit parameters)
trait Ordering[T] {
def compare(x: T, y: T): Int
}
implicit val intOrdering: Ordering[Int] = new Ordering[Int] {
def compare(x: Int, y: Int): Int = x.compareTo(y)
}
implicit val stringOrdering: Ordering[String] = new Ordering[String] {
def compare(x: String, y: String): Int = x.compareTo(y)
}
// Context bound: T : Ordering means there must be an implicit Ordering[T]
class Sorter[T : Ordering] {
def sort(items: List[T]): List[T] = {
val ordering = implicitly[Ordering[T]]
items.sortWith((a, b) => ordering.compare(a, b) < 0)
}
}
val intSorter = new Sorter[Int]
val sortedInts = intSorter.sort(List(3, 1, 4, 1, 5, 9))
println(s"Sorted integers: $sortedInts")
val stringSorter = new Sorter[String]
val sortedStrings = stringSorter.sort(List("banana", "apple", "cherry"))
println(s"Sorted strings: $sortedStrings")
// Multiple bounds
trait Serializable {
def toBytes: Array[Byte]
}
trait Comparable[T] {
def compareTo(other: T): Int
}
// Multiple upper bounds with intersection types
class PersistentStore[T <: Serializable with Comparable[T]] {
private var items = List.empty[T]
def store(item: T): Unit = {
items = item :: items
// Can use both Serializable and Comparable methods
val bytes = item.toBytes
println(s"Stored item with ${bytes.length} bytes")
}
def findMin(): Option[T] = {
items.reduceOption((a, b) => if (a.compareTo(b) <= 0) a else b)
}
}
// Implementing the required traits
case class ScoredItem(name: String, score: Int)
extends Serializable with Comparable[ScoredItem] {
def toBytes: Array[Byte] = s"$name:$score".getBytes
def compareTo(other: ScoredItem): Int = score.compareTo(other.score)
}
val store = new PersistentStore[ScoredItem]
store.store(ScoredItem("Alice", 95))
store.store(ScoredItem("Bob", 87))
store.store(ScoredItem("Charlie", 92))
println(s"Minimum score item: ${store.findMin()}")
// View bounds (deprecated but important to understand)
// T <% ViewedAsType means there's an implicit conversion from T to ViewedAsType
// Type bounds with type members
trait Collection {
type Element
type Self <: Collection
def add(element: Element): Self
def size: Int
}
trait SortedCollection extends Collection {
// Self type must be a SortedCollection and Element must be Comparable
self: Self =>
type Element <: Comparable[Element]
def min(): Option[Element]
def max(): Option[Element]
}
// Abstract type bounds
trait TypeBoundedContainer[T] {
type Contained <: T // Contained must be a subtype of T
def create(value: T): Contained
def extract(container: Contained): T
}
class StringContainer extends TypeBoundedContainer[CharSequence] {
type Contained = String
def create(value: CharSequence): String = value.toString
def extract(container: String): CharSequence = container
}
val stringContainer = new StringContainer
val contained = stringContainer.create(new StringBuilder("Hello"))
println(s"Contained: $contained")
// Bounded wildcards (existential types)
trait Processor {
type Input
type Output
def process(input: Input): Output
}
// Working with processors without knowing exact types
def runProcessor(processor: Processor)(input: processor.Input): processor.Output = {
processor.process(input)
}
class StringToIntProcessor extends Processor {
type Input = String
type Output = Int
def process(input: String): Int = input.length
}
val processor = new StringToIntProcessor
val result = runProcessor(processor)("Hello World")
println(s"String length: $result")
// F-bounded polymorphism (recursive type bounds)
trait Comparable[T <: Comparable[T]] {
def compareTo(other: T): Int
def lessThan(other: T): Boolean = compareTo(other) < 0
}
case class Version(major: Int, minor: Int, patch: Int) extends Comparable[Version] {
def compareTo(other: Version): Int = {
val majorCmp = major.compareTo(other.major)
if (majorCmp != 0) majorCmp
else {
val minorCmp = minor.compareTo(other.minor)
if (minorCmp != 0) minorCmp
else patch.compareTo(other.patch)
}
}
}
val v1 = Version(1, 2, 3)
val v2 = Version(1, 3, 0)
println(s"v1 < v2: ${v1.lessThan(v2)}")
// Self-type bounds
trait Component { self =>
type Config
def configure(config: Config): self.type
}
trait DatabaseComponent extends Component {
type Config = DatabaseConfig
def configure(config: DatabaseConfig): this.type = {
println(s"Configuring database: ${config.url}")
this
}
}
case class DatabaseConfig(url: String, username: String, password: String)
trait LoggingComponent extends Component {
type Config = LoggingConfig
def configure(config: LoggingConfig): this.type = {
println(s"Configuring logging: ${config.level}")
this
}
}
case class LoggingConfig(level: String, file: String)
// Combining components with self-type bounds
trait Application extends DatabaseComponent with LoggingComponent {
// Must resolve Config conflict
type Config = (DatabaseConfig, LoggingConfig)
override def configure(config: (DatabaseConfig, LoggingConfig)): this.type = {
super[DatabaseComponent].configure(config._1)
super[LoggingComponent].configure(config._2)
this
}
}
class MyApplication extends Application
val app = new MyApplication
app.configure((
DatabaseConfig("jdbc:postgresql://localhost/db", "user", "pass"),
LoggingConfig("INFO", "/var/log/app.log")
))
Higher-Kinded Types
Type Constructors and Higher-Kinded Types
// Higher-kinded types: types that take other types as parameters
trait Container[F[_]] {
def wrap[A](value: A): F[A]
def unwrap[A](container: F[A]): A
}
// Option container
implicit val optionContainer = new Container[Option] {
def wrap[A](value: A): Option[A] = Some(value)
def unwrap[A](container: Option[A]): A = container.get
}
// List container
implicit val listContainer = new Container[List] {
def wrap[A](value: A): List[A] = List(value)
def unwrap[A](container: List[A]): A = container.head
}
// Generic function that works with any container
def processWithContainer[F[_], A](value: A)(implicit container: Container[F]): F[A] = {
container.wrap(value)
}
val optionResult = processWithContainer[Option, String]("hello")
val listResult = processWithContainer[List, Int](42)
println(s"Option result: $optionResult")
println(s"List result: $listResult")
// Functor: a higher-kinded type class
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
implicit val optionFunctor2 = new Functor[Option] {
def map[A, B](fa: Option[A])(f: A => B): Option[B] = fa.map(f)
}
implicit val listFunctor2 = new Functor[List] {
def map[A, B](fa: List[A])(f: A => B): List[B] = fa.map(f)
}
// Generic map function
def mapContainer[F[_], A, B](container: F[A])(f: A => B)(implicit functor: Functor[F]): F[B] = {
functor.map(container)(f)
}
val mappedOption = mapContainer(Some(10))(_ * 2)
val mappedList = mapContainer(List(1, 2, 3))(_ + 1)
println(s"Mapped option: $mappedOption")
println(s"Mapped list: $mappedList")
// Monad: more powerful than Functor
trait Monad[F[_]] extends Functor[F] {
def pure[A](value: A): F[A]
def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
// Default implementation of map using flatMap and pure
def map[A, B](fa: F[A])(f: A => B): F[B] = flatMap(fa)(a => pure(f(a)))
}
implicit val optionMonad = new Monad[Option] {
def pure[A](value: A): Option[A] = Some(value)
def flatMap[A, B](fa: Option[A])(f: A => Option[B]): Option[B] = fa.flatMap(f)
}
implicit val listMonad = new Monad[List] {
def pure[A](value: A): List[A] = List(value)
def flatMap[A, B](fa: List[A])(f: A => List[B]): List[B] = fa.flatMap(f)
}
// Generic monadic operations
def sequence[F[_], A](containers: List[F[A]])(implicit monad: Monad[F]): F[List[A]] = {
containers.foldRight(monad.pure(List.empty[A])) { (fa, acc) =>
monad.flatMap(fa) { a =>
monad.map(acc)(a :: _)
}
}
}
val optionList = List(Some(1), Some(2), Some(3))
val sequencedOptions = sequence(optionList)
println(s"Sequenced options: $sequencedOptions")
val listOfLists = List(List(1, 2), List(3, 4), List(5, 6))
val sequencedLists = sequence(listOfLists)
println(s"Sequenced lists: $sequencedLists")
// Higher-kinded types with multiple type parameters
trait BiContainer[F[_, _]] {
def wrap[A, B](a: A, b: B): F[A, B]
def first[A, B](container: F[A, B]): A
def second[A, B](container: F[A, B]): B
}
implicit val tupleContainer = new BiContainer[Tuple2] {
def wrap[A, B](a: A, b: B): (A, B) = (a, b)
def first[A, B](container: (A, B)): A = container._1
def second[A, B](container: (A, B)): B = container._2
}
case class Pair[A, B](left: A, right: B)
implicit val pairContainer = new BiContainer[Pair] {
def wrap[A, B](a: A, b: B): Pair[A, B] = Pair(a, b)
def first[A, B](container: Pair[A, B]): A = container.left
def second[A, B](container: Pair[A, B]): B = container.right
}
def createPair[F[_, _], A, B](a: A, b: B)(implicit container: BiContainer[F]): F[A, B] = {
container.wrap(a, b)
}
val tuplePair = createPair[Tuple2, String, Int]("hello", 42)
val customPair = createPair[Pair, String, Int]("world", 24)
println(s"Tuple pair: $tuplePair")
println(s"Custom pair: $customPair")
// Type lambda (anonymous type function)
trait Foldable[F[_]] {
def foldLeft[A, B](fa: F[A], zero: B)(f: (B, A) => B): B
def foldRight[A, B](fa: F[A], zero: B)(f: (A, B) => B): B
}
// For Either, we need to fix one type parameter
type EitherString[A] = Either[String, A]
implicit val eitherFoldable = new Foldable[EitherString] {
def foldLeft[A, B](fa: Either[String, A], zero: B)(f: (B, A) => B): B =
fa.fold(_ => zero, a => f(zero, a))
def foldRight[A, B](fa: Either[String, A], zero: B)(f: (A, B) => B): B =
fa.fold(_ => zero, a => f(a, zero))
}
def sum[F[_], A](fa: F[A])(implicit foldable: Foldable[F], num: Numeric[A]): A = {
foldable.foldLeft(fa, num.zero)(num.plus)
}
val rightValue: Either[String, Int] = Right(42)
val leftValue: Either[String, Int] = Left("error")
// Note: This would sum single values, more useful with collections
// println(s"Sum of right: ${sum(rightValue)}")
// Higher-kinded types for generic programming
trait Traversable[F[_]] extends Functor[F] with Foldable[F] {
def traverse[G[_], A, B](fa: F[A])(f: A => G[B])(implicit applicative: Applicative[G]): G[F[B]]
}
trait Applicative[F[_]] extends Functor[F] {
def pure[A](value: A): F[A]
def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]
}
implicit val optionApplicative = new Applicative[Option] {
def pure[A](value: A): Option[A] = Some(value)
def ap[A, B](ff: Option[A => B])(fa: Option[A]): Option[B] =
for { f <- ff; a <- fa } yield f(a)
def map[A, B](fa: Option[A])(f: A => B): Option[B] = fa.map(f)
}
// Kind projector syntax (if available)
// type EitherStringKP[A] = Either[String, A]
// Or using lambda syntax: ({type L[A] = Either[String, A]})#L
// Practical example: Generic validation
sealed trait Validation[+A]
case class Success[A](value: A) extends Validation[A]
case class Failure(errors: List[String]) extends Validation[Nothing]
implicit val validationApplicative = new Applicative[Validation] {
def pure[A](value: A): Validation[A] = Success(value)
def ap[A, B](ff: Validation[A => B])(fa: Validation[A]): Validation[B] =
(ff, fa) match {
case (Success(f), Success(a)) => Success(f(a))
case (Failure(errors1), Failure(errors2)) => Failure(errors1 ++ errors2)
case (Failure(errors), _) => Failure(errors)
case (_, Failure(errors)) => Failure(errors)
}
def map[A, B](fa: Validation[A])(f: A => B): Validation[B] = fa match {
case Success(value) => Success(f(value))
case Failure(errors) => Failure(errors)
}
}
def validatePositive(n: Int): Validation[Int] =
if (n > 0) Success(n) else Failure(List(s"$n is not positive"))
def validateEven(n: Int): Validation[Int] =
if (n % 2 == 0) Success(n) else Failure(List(s"$n is not even"))
// Combining validations
val validation1 = validatePositive(-5)
val validation2 = validateEven(3)
println(s"Validation 1: $validation1")
println(s"Validation 2: $validation2")
// Type-level programming example
trait Nat
class Zero extends Nat
class Succ[N <: Nat] extends Nat
type One = Succ[Zero]
type Two = Succ[One]
type Three = Succ[Two]
trait ToInt[N <: Nat] {
def value: Int
}
implicit val zeroToInt: ToInt[Zero] = new ToInt[Zero] {
def value: Int = 0
}
implicit def succToInt[N <: Nat](implicit prev: ToInt[N]): ToInt[Succ[N]] =
new ToInt[Succ[N]] {
def value: Int = prev.value + 1
}
def natToInt[N <: Nat](implicit toInt: ToInt[N]): Int = toInt.value
println(s"Zero: ${natToInt[Zero]}")
println(s"Three: ${natToInt[Three]}")
Advanced Type Patterns
Type Classes and Implicits
// Type class pattern
trait Show[T] {
def show(value: T): String
}
// Type class instances
implicit val intShow: Show[Int] = new Show[Int] {
def show(value: Int): String = value.toString
}
implicit val stringShow: Show[String] = new Show[String] {
def show(value: String): String = s"\"$value\""
}
implicit val booleanShow: Show[Boolean] = new Show[Boolean] {
def show(value: Boolean): String = if (value) "true" else "false"
}
// Generic function using type class
def display[T](value: T)(implicit show: Show[T]): String = show.show(value)
// Or using context bound syntax
def displayWithBound[T : Show](value: T): String = implicitly[Show[T]].show(value)
println(display(42))
println(display("hello"))
println(display(true))
// Derivation for case classes
implicit def caseClassShow[T <: Product](implicit mirror: scala.deriving.Mirror.ProductOf[T]): Show[T] =
new Show[T] {
def show(value: T): String = {
val className = value.getClass.getSimpleName
val fields = value.productIterator.toList
s"$className(${fields.mkString(", ")})"
}
}
case class Person(name: String, age: Int)
case class Address(street: String, city: String, zipCode: String)
println(display(Person("Alice", 30)))
println(display(Address("123 Main St", "Anytown", "12345")))
// Type class with multiple type parameters
trait Converter[From, To] {
def convert(value: From): To
}
implicit val stringToInt: Converter[String, Int] = new Converter[String, Int] {
def convert(value: String): Int = value.toInt
}
implicit val intToString: Converter[Int, String] = new Converter[Int, String] {
def convert(value: Int): String = value.toString
}
implicit val stringToBoolean: Converter[String, Boolean] = new Converter[String, Boolean] {
def convert(value: String): Boolean = value.toLowerCase == "true"
}
def convert[From, To](value: From)(implicit converter: Converter[From, To]): To =
converter.convert(value)
println(convert[String, Int]("42"))
println(convert[Int, String](42))
println(convert[String, Boolean]("true"))
// Type class with laws and properties
trait Monoid[T] {
def empty: T
def combine(x: T, y: T): T
// Laws (not enforced by compiler, but should hold):
// 1. Associativity: combine(combine(x, y), z) == combine(x, combine(y, z))
// 2. Left identity: combine(empty, x) == x
// 3. Right identity: combine(x, empty) == x
}
implicit val stringMonoid: Monoid[String] = new Monoid[String] {
def empty: String = ""
def combine(x: String, y: String): String = x + y
}
implicit val intMonoid: Monoid[Int] = new Monoid[Int] {
def empty: Int = 0
def combine(x: Int, y: Int): Int = x + y
}
implicit def listMonoid[T]: Monoid[List[T]] = new Monoid[List[T]] {
def empty: List[T] = List.empty
def combine(x: List[T], y: List[T]): List[T] = x ++ y
}
def fold[T](values: List[T])(implicit monoid: Monoid[T]): T =
values.foldLeft(monoid.empty)(monoid.combine)
println(fold(List("hello", " ", "world")))
println(fold(List(1, 2, 3, 4, 5)))
println(fold(List(List(1, 2), List(3, 4), List(5, 6))))
// Type class hierarchy
trait Semigroup[T] {
def combine(x: T, y: T): T
}
trait MonoidFromSemigroup[T] extends Semigroup[T] {
def empty: T
}
implicit def monoidFromSemigroup[T](implicit sg: Semigroup[T], emp: Empty[T]): MonoidFromSemigroup[T] =
new MonoidFromSemigroup[T] {
def combine(x: T, y: T): T = sg.combine(x, y)
def empty: T = emp.empty
}
trait Empty[T] {
def empty: T
}
implicit val stringEmpty: Empty[String] = new Empty[String] {
def empty: String = ""
}
implicit val stringSemigroup: Semigroup[String] = new Semigroup[String] {
def combine(x: String, y: String): String = x + y
}
// Advanced type class usage with syntax extension
implicit class ShowOps[T](value: T)(implicit show: Show[T]) {
def show: String = show.show(value)
}
implicit class MonoidOps[T](value: T)(implicit monoid: Monoid[T]) {
def |+|(other: T): T = monoid.combine(value, other)
}
// Using syntax extensions
println(42.show)
println("hello".show)
println("hello" |+| " " |+| "world")
println(List(1, 2) |+| List(3, 4))
// Type-directed programming
trait Default[T] {
def default: T
}
implicit val defaultInt: Default[Int] = new Default[Int] {
def default: Int = 0
}
implicit val defaultString: Default[String] = new Default[String] {
def default: String = ""
}
implicit val defaultBoolean: Default[Boolean] = new Default[Boolean] {
def default: Boolean = false
}
implicit def defaultList[T]: Default[List[T]] = new Default[List[T]] {
def default: List[T] = List.empty
}
implicit def defaultOption[T]: Default[Option[T]] = new Default[Option[T]] {
def default: Option[T] = None
}
def getDefault[T](implicit default: Default[T]): T = default.default
println(s"Default Int: ${getDefault[Int]}")
println(s"Default String: '${getDefault[String]}'")
println(s"Default List[String]: ${getDefault[List[String]]}")
println(s"Default Option[Int]: ${getDefault[Option[Int]]}")
// Phantom types for type-safe APIs
trait UnitType
trait Meter extends UnitType
trait Foot extends UnitType
trait Second extends UnitType
case class Measurement[U <: UnitType](value: Double) {
def +(other: Measurement[U]): Measurement[U] =
Measurement(value + other.value)
def -(other: Measurement[U]): Measurement[U] =
Measurement(value - other.value)
def *(scalar: Double): Measurement[U] =
Measurement(value * scalar)
}
object Measurement {
def meters(value: Double): Measurement[Meter] = Measurement[Meter](value)
def feet(value: Double): Measurement[Foot] = Measurement[Foot](value)
def seconds(value: Double): Measurement[Second] = Measurement[Second](value)
}
// Type-safe conversions
trait Converter[From <: UnitType, To <: UnitType] {
def convert(measurement: Measurement[From]): Measurement[To]
}
implicit val meterToFoot: Converter[Meter, Foot] = new Converter[Meter, Foot] {
def convert(measurement: Measurement[Meter]): Measurement[Foot] =
Measurement[Foot](measurement.value * 3.28084)
}
implicit val footToMeter: Converter[Foot, Meter] = new Converter[Foot, Meter] {
def convert(measurement: Measurement[Foot]): Measurement[Meter] =
Measurement[Meter](measurement.value / 3.28084)
}
def convert[From <: UnitType, To <: UnitType](measurement: Measurement[From])
(implicit converter: Converter[From, To]): Measurement[To] =
converter.convert(measurement)
val distance1 = Measurement.meters(10.0)
val distance2 = Measurement.meters(5.0)
val totalDistance = distance1 + distance2
println(s"Total distance: ${totalDistance.value} meters")
val distanceInFeet = convert(totalDistance)
println(s"Total distance: ${distanceInFeet.value} feet")
// Cannot add different units (compile-time safety)
// val time = Measurement.seconds(30.0)
// val invalid = distance1 + time // Compile error!
Summary
In this lesson, you've mastered advanced type system features:
✅ Variance: Covariance, contravariance, and invariance patterns
✅ Type Bounds: Upper bounds, lower bounds, and context bounds
✅ Higher-Kinded Types: Type constructors and generic abstractions
✅ Type Classes: Flexible, composable abstractions
✅ Advanced Patterns: F-bounded polymorphism and phantom types
✅ Type Safety: Building APIs that prevent runtime errors
✅ Generic Programming: Creating reusable, type-safe libraries
These advanced type features enable you to build sophisticated, type-safe APIs and libraries that catch errors at compile time while remaining flexible and expressive.
What's Next
In the next lesson, we'll explore implicits and type classes in greater depth, learning how to build powerful DSLs and create elegant APIs that feel natural to use while maintaining strong type safety.
Comments
Be the first to comment on this lesson!