Scala
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Scala
-
Section 4: Functional Scala
-
Section 5: Collections in Depth
-
Section 6: Type System
-
Section 7: Concurrency and Ecosystem
-
Section 8: Practical Projects
-
Section 9: Interview Practice
-
Section 10: Data Structures and Algorithms in Scala
-
Section 11: More Practice Exercises
-
Section 12: Advanced Functional Patterns
-
Section 13: More Ecosystem
-
Section 14: Scala Collections Library Deep Dive
-
Section 15: Scala Standard Library Deep Dive
-
Section 16: Akka Ecosystem Deep Dive
-
Section 17: Cats and Cats Effect Deep Dive
-
Section 18: Play Framework Deep Dive
-
Section 19: Apache Spark with Scala Deep Dive
-
Section 20: Scala Build Tools Deep Dive
-
Section 21: Scala 3 Specific Features
-
90: Union and Intersection Types
-
Section 22: Scala Testing Deep Dive
-
Section 23: Functional Domain Modeling
-
Section 24: More Data Structures and Algorithms in Scala
-
Section 25: Scala for Data Engineering
-
Section 26: More Practical Projects
-
Section 27: More Interview and Review
-
Section 28: ZIO Ecosystem Deep Dive
-
Section 29: Scala for Machine Learning
-
Section 30: Scala Microservices Architecture
-
Section 31: Scala Type System Deep Dive
-
Section 32: More Practice and Drills
-
Section 33: Scala Performance Deep Dive
-
Section 34: Scala Ecosystem Tooling
-
Section 35: Scala for Reactive Systems
-
Section 36: More Real-World Case Studies
-
Section 37: Scala for Financial Systems
-
Section 38: Scala GraphQL and gRPC
-
Section 39: More Final Projects
-
Section 40: More Interview and Final Review
-
Section 41: Scala for Streaming Data
-
Section 42: Scala Security Practices
-
Section 43: More Language Deep Dive
-
Section 44: Scala Command-Line Tools
-
Section 45: Scala Documentation and Style
-
Section 46: Scala Dependency Management
-
Section 47: More Practical Backend Patterns
-
Section 48: Scala for Event-Driven Architecture
-
Section 49: More Practice Drills Round 2
-
Section 50: Scala Compiler Deep Dive
-
Section 51: Scala for Web Frontends
-
Section 52: More Data Engineering Practice
-
Section 53: Scala Observability
-
Section 54: More Advanced Practice Projects
-
Section 55: Scala for Legacy Java Integration
-
Section 56: More Testing Practice
-
Section 57: Final Mastery Review
-
Section 58: Scala History and Ecosystem Context
-
Section 59: More Concurrency Patterns
-
Section 60: Scala for Configuration Management
-
Section 61: More Domain Modeling Practice
28: Type Classes in Scala
Up until now, you've likely handled polymorphism by defining a trait and having your classes extend it. That works great when you own the code and the relationship is a strict "is-a" relationship. But what happens when you want to add behavior to a class you didn't write—like Int or String—or when you want to keep your domain models completely clean of infrastructure concerns like JSON serialization or database mapping?
That's where type classes come in. In Scala, a type class is essentially a pattern that lets us achieve ad-hoc polymorphism. Instead of saying "a User is a JSON-serializable object," we say "there exists a way to serialize a User to JSON."
Defining the behavior we want
Let's build a simple JSON writer. We don't want to use a library for this; we want to see how the machinery works. First, we define the "contract" for our type class. This is just a trait with a type parameter.
trait JsonWriter[T] {
def write(value: T): String
}
Notice that JsonWriter isn't something a class inherits from. It's a separate entity that describes a capability for a type T.
The temptation to bake logic into the data
When I first started implementing this in a project a few years back, I did something that felt intuitive but ended up being a nightmare. I tried to make my domain models implement a Serializable trait directly.
trait JsonSerializable {
def toJson: String
}
case class User(name: String, age: Int) extends JsonSerializable {
def toJson: String = s"""{"name": "$name", "age": $age}"""
}
At first glance, this is easy. But then I realized I'd just polluted my User model with JSON logic. If I later needed to support XML, Protobuf, and YAML, my User class would become a bloated mess of serialization methods. Worse, I couldn't do this for Int or List because I can't go into the Scala standard library and make Int extend my JsonSerializable trait.
Separating the 'what' from the 'how'
To fix this, we move the implementation outside the class. We create "instances" of our type class for the types we care about. We mark these as implicit so the compiler can find them automatically when needed.
case class User(name: String, age: Int)
// Implementation for String
implicit val stringWriter = new JsonWriter[String] {
def write(value: String): String = s""""$value""""
}
// Implementation for Int
implicit val intWriter = new JsonWriter[Int] {
def write(value: Int): String = value.toString
}
// Implementation for User
implicit val userWriter = new JsonWriter[User] {
def write(user: User): String =
s"""{"name": ${stringWriter.write(user.name)}, "age": ${intWriter.write(user.age)}}"""
}
Now, the User class is just a data carrier. It has no idea that JsonWriter even exists. This is much cleaner.
Making it ergonomic with a helper
Writing userWriter.write(myUser) is fine, but it's clunky. We want a way to just say "write this thing" and let the compiler figure out which instance to use. We can do this by creating a helper method that requires an implicit JsonWriter.
object Json {
def write[T](value: T)(implicit writer: JsonWriter[T]): String = {
writer.write(value)
}
}
// Now we can use it like this:
val user = User("Alice", 30)
println(Json.write(user)) // The compiler finds userWriter automatically
println(Json.write("Hello")) // The compiler finds stringWriter automatically
What's happening here is that the compiler looks at the type of value (e.g., User), then searches the implicit scope for a JsonWriter[User]. If it finds one, it plugs it in. If it doesn't, you get a compile-time error. This gives us total type safety without sacrificing the flexibility of our data models.
📋 Practical Task
Implementing a Custom Formatter for a Product Catalog
You are building a catalog system. You have a Product case class and you need a way to format these products for different output targets (e.g., a concise summary for a mobile app and a detailed view for a website).
Your task:
- Define a type class trait called
ProductFormatter[T]with a methodformat(item: T): String. - Create a case class
Product(id: Int, name: String, price: Double). - Create an implicit instance of
ProductFormatter[Product]that returns a string in the format:"Product: [name] - $[price]". - Create an implicit instance of
ProductFormatter[Int](to handle formatting just the ID) that returns:"ID: [id]". - Implement a helper object
Formatterwith a methoddisplay[T](item: T)(implicit f: ProductFormatter[T]): Stringthat calls the formatter. - Test your implementation by calling
Formatter.displayon both aProductinstance and anInt.
There are no comments for now.