Type Classes
A type class is the functional equivalent of polymorphism in the object-oriented world.
And it is resolved in compile-time by importing context givens
The Object-Oriented Way
trait Show:
def show: String
case class Person(name: String, age: Int) extends Show:
override def show = s"$name with age $age"
val john = Person("John", 28 )
val showJohn = john.show
- available only for the types we
extend - provide only one
overrideimplementation
The Pattern Matching Way
def show(value: Any): String = value match
case Person(name, age) => s"$name with age $age"
case _ => throw new IllegalArgumentException("not supported")
- lose type safety
- extends match for every pattern
- has one
caseimplementation
Type Classes
Create a type class has the following steps
- Type Class definition
- Type Class instances
- the API
- Extension methods
Type Class Definition
trait Shower[T] {
def show(value: T): String
}
Type Class Instances
given userShower: Shower[Person] with
override def show(value: Person) =
val Person(name, age) = value
s"$name with age $age"}
val john = Person("John", 28)
val showJohn = userShower.show(john)
API
object Shower {
def show[T](value: T)(using shower: Shower[T]): String =
shower.show(value)
def apply[T](using shower: Shower[T]): Shower[T] = shower
}
val showJohn = Shower.show(john)
Extension method
object ShowSyntax:
extension [T](value: T)
def show(using shower: Shower[T]): String = shower.show(value)
import ShowSyntax.*
val showJohn = john.show
- can define showers for other types
- multiple shower for the same type
Example Dog ad Cat
In Scala 3, the mechanism for defining and using type classes has been significantly revamped with the introduction of given and using clauses, replacing the older implicit keyword. This change aims to make the definition and usage of type classes more explicit and readable, addressing some of the common criticisms of Scala's implicit system.
Defining Type Classes with given
To define a type class instance with Scala 3, you use the given keyword. This replaces the implicit val or implicit object definitions used in Scala 2.
Example: Defining a SoundMaker type class and its instances.
trait SoundMaker[T]:
def makeSound(value: T): Unit
// Define instances of the SoundMaker type class
given SoundMaker[Dog] with
def makeSound(dog: Dog): Unit = println("Woof")
given SoundMaker[Cat] with
def makeSound(cat: Cat): Unit = println("Meow")
Here, given declarations define how Dog and Cat types fulfill the SoundMaker contract. These instances are globally available and automatically used wherever a SoundMaker[T] is required.
Using Type Classes with using
To use a type class instance, Scala 3 introduces the using clause. This explicitly specifies that a function requires a type class instance for its operation, making the function's dependencies clear.
Example: Using the SoundMaker type class to implement a polymorphic playSound function.
def playSound[T](value: T)(using maker: SoundMaker[T]): Unit = maker.makeSound(value)
The using clause in the function signature tells the compiler to search for an implicit SoundMaker[T] instance for whatever type T is passed into the function. This search is based on the instances defined with given.
Putting It All Together
class Dog(val name: String)
class Cat(val name: String)
// Assuming the given instances and the playSound method are defined as above
val myDog = Dog("Rex")
val myCat = Cat("Whiskers")
playSound(myDog) // Outputs: Woof
playSound(myCat) // Outputs: Meow