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
31: Self Types and Structural Types
Today we're diving into two features that often feel like "magic" until you see them solve a specific architectural problem: self types and structural types. I like to think of these as tools for defining requirements. Instead of just saying "this class inherits from that class," we're saying "this piece of logic requires these other capabilities to exist."
Let's build a simple combat system for a game. We have a basic Entity, and we want to add a Healable behavior. But here's the catch: it doesn't make sense to "heal" something that doesn't have health stats. We don't want to force every single Entity to have health (maybe a "Rock" entity shouldn't), but we want the Healable trait to be able to manipulate health directly.
Constraining traits with self types
Usually, if I wanted Healable to access health, I'd make Healable extend HealthStats. But that's too rigid. I want Healable to be a mixin that I can plug into any class, as long as that class also provides health stats.
trait HealthStats {
var currentHealth: Int
var maxHealth: Int
}
trait Healable {
// This is the self type.
// It says: "I can only be mixed into a class that is also a HealthStats"
self: HealthStats =>
def heal(amount: Int): Unit = {
currentHealth = Math.min(currentHealth + amount, maxHealth)
println(s"Healed! Current health is now $currentHealth")
}
}
Notice that inside heal, I'm accessing currentHealth and maxHealth directly. I didn't define them in Healable, and I'm not casting this to a HealthStats. Because of the self type, the compiler knows that whatever instance implements Healable is guaranteed to be a HealthStats too.
Where I messed up the hierarchy
When I first wrote this, I tried to be "clever" and create a Player class that implemented Healable but forgot to implement HealthStats. I thought I could just override the health methods manually. Here is what I tried:
class Player(name: String) extends Healable {
var currentHealth = 100
var maxHealth = 100
}
The compiler immediately yelled at me. It told me that Player must be a subtype of HealthStats. I realized I had fallen into the trap of thinking that having the same methods is the same as having the same type. In Scala, inheritance is explicit. To fix this, I have to explicitly extend the trait the self-type requires:
class Player(name: String) extends HealthStats with Healable {
override var currentHealth = 100
override var maxHealth = 100
}
Now it works. The self type acts as a contract: "You can have my healing logic, but only if you provide the health data structure I expect."
Handling third-party objects with structural types
Now, let's talk about structural types. These are different. While self types are about inheritance, structural types are about shape. They are Scala's version of "duck typing."
Imagine we want to log combat events. We might have a FileLogger, a ConsoleLogger, or maybe a third-party logging library that we can't change (meaning we can't make it extend our own Logger trait). We just know that whatever we use has a log(message: String) method.
def reportCombatEvent(event: String, logger: { def log(s: String): Unit }) = {
logger.log(s"[COMBAT] $event")
}
That weird syntax { def log(s: String): Unit } is a structural type. I'm telling Scala: "I don't care what the class hierarchy of logger is. As long as it has a method called log that takes a string and returns unit, it's welcome here."
I'll be honest: use these sparingly. Structural types are implemented via reflection at runtime in Scala 2, which makes them significantly slower than regular trait-based polymorphism. But when you're dealing with an API you don't control, they are a lifesaver. They allow you to maintain decoupling without forcing every single helper class in your system to inherit from a massive tree of traits.
📋 Practical Task
Exercise: Implementing a Structural Event Dispatcher
You are building a notification system. You have a function called sendAlert that needs to take an object capable of sending a message, but you don't want to define a formal Notifier trait because the objects being passed in come from different external modules.
Requirements:
- Create a function
sendAlert(message: String, dispatcher: { def dispatch(msg: String): Unit }). - Create a class
EmailServicethat has a methoddispatch(msg: String)but does not implement any specific interface. - Create a class
SmsServicethat also has a methoddispatch(msg: String)but does not implement any specific interface. - In your main logic, call
sendAlertusing an instance ofEmailServiceand then again using an instance ofSmsService.
This will prove that your sendAlert function is decoupled from the specific class hierarchy and relies only on the structure of the objects passed to it.
There are no comments for now.