Skip to Content
Course content

31: Self Types and Structural Types

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 EmailService that has a method dispatch(msg: String) but does not implement any specific interface.
  • Create a class SmsService that also has a method dispatch(msg: String) but does not implement any specific interface.
  • In your main logic, call sendAlert using an instance of EmailService and then again using an instance of SmsService.

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.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.