scalaintermediate

Multiversal Equality in Scala 3

Use strict equality with CanEqual to prevent comparing unrelated types at compile time.

scala
import scala.language.strictEquality

// With strict equality, this won't compile:
// val x: Int = 1
// val s: String = "1"
// x == s  // Error! Can't compare Int and String

// Opt-in equality
case class UserId(value: Long) derives CanEqual
case class OrderId(value: Long) derives CanEqual

// UserId(1) == OrderId(1)  // Won't compile!
// UserId(1) == UserId(1)   // OK

// Custom CanEqual for related types
sealed trait Currency derives CanEqual
case object USD extends Currency
case object EUR extends Currency
case object GBP extends Currency

case class Money(amount: Double, currency: Currency) derives CanEqual

// Allow comparing Money instances
given CanEqual[Money, Money] = CanEqual.derived

// Enum with equality
enum Color derives CanEqual:
  case Red, Green, Blue

enum Size derives CanEqual:
  case Small, Medium, Large

// Color.Red == Size.Small  // Won't compile! Different types

// With inheritance
sealed trait Shape derives CanEqual
case class Circle(r: Double) extends Shape
case class Square(s: Double) extends Shape

// Allows Shape subtypes to be compared
given CanEqual[Shape, Shape] = CanEqual.derived

@main def run(): Unit =
  // Same type comparison works
  val id1 = UserId(1)
  val id2 = UserId(1)
  val id3 = UserId(2)
  println(s"id1 == id2: ${id1 == id2}")  // true
  println(s"id1 == id3: ${id1 == id3}")  // false

  // Currency comparison
  println(s"USD == USD: ${USD == USD}")  // true
  println(s"USD == EUR: ${USD == EUR}")  // false

  // Money comparison
  val m1 = Money(100, USD)
  val m2 = Money(100, USD)
  val m3 = Money(100, EUR)
  println(s"m1 == m2: ${m1 == m2}")  // true
  println(s"m1 == m3: ${m1 == m3}")  // false

  // Shape comparison
  val shapes: List[Shape] = List(Circle(5), Square(3), Circle(5))
  println(s"Same circle: ${shapes(0) == shapes(2)}")
  println(s"Circle vs Square: ${shapes(0) == shapes(1)}")

  // Pattern matching still works
  val color: Color = Color.Red
  color match
    case Color.Red   => println("Red!")
    case Color.Green => println("Green!")
    case Color.Blue  => println("Blue!")

Use Cases

  • Preventing accidental type comparisons
  • Domain type safety
  • Compile-time equality checking

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.