Skip to Content
Course content

174: Structural Types in Scala

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

I've run into a specific headache a few times when integrating third-party Java libraries into Scala projects: the "almost-compatible" object. You find two or three different classes from different packages that all happen to have the exact same method signature, but they don't share a common interface or parent class. In a perfect world, they'd all implement a trait I defined, but since I didn't write the library, I can't just go in and add extends MyTrait to their source code.

The "Ideal" Interface

Let's say I'm building a notification dispatcher. I want a function that can take any "reporter" and send a message. My first instinct—the one that works 90% of the time—is to define a trait and require that trait as the parameter type.

trait MessageReporter {
  def report(message: String): Unit
}

def dispatch(reporter: MessageReporter, msg: String): Unit = {
  reporter.report(msg)
}

This is clean. It's type-safe. It's exactly how we're taught to write Scala. But here's where the reality of messy dependencies kicks in.

The Reality of Third-Party Libraries

Imagine I'm using two different libraries. One provides an EmailService and the other provides a SmsService. Both have a report method, but they are completely unrelated in the class hierarchy.

class EmailService {
  def report(message: String): Unit = println(s"Sending Email: $message")
}

class SmsService {
  def report(message: String): Unit = println(s"Sending SMS: $message")
}

val email = new EmailService()
val sms = new SmsService()

// This is where I hit the wall:
dispatch(email, "Hello!") // Type mismatch: Expected MessageReporter, found EmailService
dispatch(sms, "Hello!")    // Type mismatch: Expected MessageReporter, found SmsService

I tried to be too rigid. I assumed I could force these classes into my MessageReporter trait, but I can't. I could write "adapter" classes for every single service—wrapping the EmailService inside a MessageReporter—but if I have twenty different services, that's a lot of boilerplate just to call one method.

Breaking the Hierarchy with Structural Types

This is where structural types come in. Instead of saying "this object must be a MessageReporter," I can say "this object must have a method called report that takes a String and returns Unit."

In Scala, we do this by defining the type as a set of members enclosed in curly braces. I'll rewrite the dispatch function like this:

def dispatch(reporter: { def report(message: String): Unit }, msg: String): Unit = {
  reporter.report(msg)
}

// Now this works perfectly:
dispatch(email, "Hello via Structural Type!")
dispatch(sms, "Hello via Structural Type!")

Notice how I didn't have to change EmailService or SmsService. I'm basically telling the compiler: "I don't care what this object is, as long as it has this specific method, let it through." It's very similar to "duck typing" in Python or Ruby, but we're still getting a level of compile-time check on the method signature.

The Performance Trade-off

Now, a word of caution. You won't see structural types used everywhere in professional codebases, and for a good reason. Because the compiler can't know at compile-time exactly which class will be passed in (since any class in the universe could have a report method), Scala implements this using runtime reflection.

Every time you call reporter.report(msg) in the example above, Scala is essentially asking the JVM at runtime, "Does this object have a method named 'report' with these arguments?" This is significantly slower than a standard method call on a trait. If you're calling this in a tight loop thousands of times per second, your performance will tank. Use structural types for high-level wiring or integration points, but stick to traits for your core business logic.




📋 Practical Task

Implementing a Universal Metric Recorder

You are integrating several different monitoring tools (Datadog, Prometheus, and a custom internal tool). Each tool provides a "client" object. None of these clients share a common interface, but all of them have a method called recordValue(name: String, value: Double): Unit.

Your task:

  1. Create two dummy classes, CloudMonitor and LocalMonitor, each with a recordValue(name: String, value: Double): Unit method that prints the value to the console.
  2. Write a function called logMetric that accepts any object containing the recordValue method (using a structural type) and a value to record.
  3. In your main code, instantiate both monitors and pass them into logMetric to verify it works without needing a shared trait.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.