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
176: Multiversal Equality in Scala 3
In Scala 2, equality was always a bit of a minefield. You had ==, which calls equals, but then you had the whole mess of Any, AnyRef, and AnyVal. If you were writing generic code, you often found yourself fighting the compiler or, worse, getting runtime results that didn't make sense because of type erasure. Scala 3 introduces "Multiversal Equality," which is a fancy way of saying the language has fundamentally changed how it handles == to be more consistent across different type "universes" (like when you're dealing with a specific type Int versus a general type Any).
Wrapping our identifiers
Let's build something concrete. Imagine we're building a system for a library, and we want to make sure we don't accidentally mix up a BookId with a MemberId, even though both are just strings under the hood. I'll start by creating a simple generic wrapper.
case class Identifier[T](value: T)
This looks innocent enough. I can now do Identifier("123") for a book and Identifier("456") for a member. Because it's a case class, Scala gives us equals and hashCode for free. If I compare two Identifier[String] objects, it works exactly as you'd expect.
The problem with generic comparisons
Here is where things get interesting. In a real app, you often have a "Search" service that doesn't know exactly what kind of identifier it's dealing with—it just knows it's some kind of Identifier[Any]. I want to write a method that checks if a provided identifier matches a target value.
def isMatch(id: Identifier[Any], target: Any): Boolean = {
id.value == target
}
In Scala 2, this was a spot where you'd start worrying about whether target was being boxed, whether the types were erased, and whether == would actually trigger the correct equals method. In Scala 3, the "multiversal" nature of equality means the compiler handles the transition between the specific type T and the universal type Any much more intelligently. It ensures that == behaves consistently regardless of whether the types are known at compile-time or only at runtime.
My attempt to force the type
Now, I'll make a mistake. I remember from my old Scala 2 days that relying on Any can be dangerous, so I'll try to "help" the compiler by overriding the equality logic to be more explicit. I'll try to implement a custom equality check that ensures the types match before comparing values.
case class Identifier[T](value: T) {
override def equals(obj: Any): Boolean = obj match {
case that: Identifier[T] => this.value == that.value
case _ => false
}
}
I thought I was being safe here, but I actually just broke the code. Because of type erasure, Identifier[T] at runtime is just Identifier. My pattern match case that: Identifier[T] doesn't actually check if T is the same; it just checks if it's any Identifier. If I compare an Identifier[Int](1) with an Identifier[String]("1"), this code might actually try to compare an Int to a String using ==, which will return false, but it's doing so in a way that's fragile and bypasses the very protections Scala 3's multiversal equality provides.
Letting the multiversal equality do the heavy lifting
The realization here is that in Scala 3, the == operator is no longer just a simple call to .equals(). It's a sophisticated operation that considers the "universe" of the types involved. When you use == on Any, Scala 3 uses a strategy that avoids the pitfalls of the old AnyRef/AnyVal split.
The fix? Delete my manual equals override and trust the case class implementation. Let's see it in action:
case class Identifier[T](value: T)
val bookId = Identifier("B123")
val memberId = Identifier("M456")
val genericId: Identifier[Any] = bookId
// This works perfectly in Scala 3
println(genericId == bookId) // true
println(genericId == memberId) // false
By removing the manual cast, I'm letting the Scala 3 compiler use its multiversal equality logic. It knows how to handle the comparison between Identifier[Any] and Identifier[String] without me having to fight the type system. I'm essentially moving from "I need to tell the compiler how to compare these" to "the compiler knows how to compare these because it understands the relationship between the types."
📋 Practical Task
Implement a Multiversal Product Key Validator
You are building a licensing system. You have a generic LicenseKey[T] case class. You need to implement a function validateKey that compares a generic LicenseKey[Any] against a known LicenseKey[String].
Requirements:
- Create a case class
LicenseKey[T](key: T). - Write a function
validateKey(input: LicenseKey[Any], expected: LicenseKey[String]): Boolean. - The function must return
trueif the keys are equal andfalseotherwise. - Constraint: Do not override the
equalsmethod in the case class; rely on Scala 3's built-in multiversal equality to handle the comparison betweenAnyandString.
Test your code with these cases:
validateKey(LicenseKey("ABC-123"), LicenseKey("ABC-123"))should betrue.validateKey(LicenseKey(12345), LicenseKey("ABC-123"))should befalse.
There are no comments for now.