The CanEqual system in Scala 3 provides compile-time guarantees that equality comparisons (==, !=) are meaningful and safe, preventing comparisons between unrelated types that would always return false.
@implicitNotFound("Values of types ${L} and ${R} cannot be compared with == or !=")
sealed trait CanEqual[-L, -R]Marker trait indicating that values of type L can be compared to values of type R. The compiler requires an implicit instance of CanEqual[L, R] for == and != operations.
object CanEqual:
object derived extends CanEqual[Any, Any]
def canEqualAny[L, R]: CanEqual[L, R]derived: Universal equality instance for when type safety is not requiredcanEqualAny: Fallback method to create equality instances for any typesgiven canEqualNumber: CanEqual[Number, Number]
given canEqualString: CanEqual[String, String]Pre-defined instances for common Java types that can be safely compared.
given canEqualSeqs[T, U](using eq: CanEqual[T, U]): CanEqual[Seq[T], Seq[U]]
given canEqualSeq[T](using eq: CanEqual[T, T]): CanEqual[Seq[T], Seq[T]]
given canEqualSet[T, U](using eq: CanEqual[T, U]): CanEqual[Set[T], Set[U]]
given canEqualMap[K1, V1, K2, V2](
using eqK: CanEqual[K1, K2], eqV: CanEqual[V1, V2]
): CanEqual[Map[K1, V1], Map[K2, V2]]
given canEqualOptions[T, U](using eq: CanEqual[T, U]): CanEqual[Option[T], Option[U]]
given canEqualOption[T](using eq: CanEqual[T, T]): CanEqual[Option[T], Option[T]]
given canEqualEither[L1, R1, L2, R2](
using eqL: CanEqual[L1, L2], eqR: CanEqual[R1, R2]
): CanEqual[Either[L1, R1], Either[L2, R2]]Type-safe equality instances for standard collection types, requiring element types to be comparable.
val str: String = "hello"
val num: Int = 42
// This compiles - strings can be compared to strings
str == "world" // true
// This would cause a compile error
// str == num // Error: Values of types String and Int cannot be compared
// Numeric types work naturally
val x: Int = 5
val y: Double = 5.0
x == y // Compiles: numeric types have built-in CanEqual instancesval list1: List[String] = List("a", "b")
val list2: List[String] = List("a", "b")
val intList: List[Int] = List(1, 2)
list1 == list2 // Compiles: both are List[String]
// list1 == intList // Error: cannot compare List[String] with List[Int]
val opt1: Option[String] = Some("hello")
val opt2: Option[String] = Some("world")
opt1 == opt2 // Compiles: both are Option[String]case class Person(name: String, age: Int)
case class Company(name: String)
val person = Person("Alice", 30)
val company = Company("Acme Corp")
// This would cause a compile error without a custom CanEqual instance
// person == company // Error: cannot compare Person with Company
// Define custom equality if needed
given CanEqual[Person, Company] = CanEqual.derived
person == company // Now compiles (though likely always false)def processOption[T](opt: Option[T]): String = opt match
case None => "empty" // Compiles: None has CanEqual[Option[T], Option[T]]
case Some(value) => s"value: $value"def compareEqual[T, U](x: T, y: U)(using CanEqual[T, U]): Boolean =
x == y
// Usage
compareEqual("hello", "world") // Compiles: String has CanEqual
compareEqual(List(1, 2), List(1, 2)) // Compiles: List[Int] has CanEqual
// compareEqual("hello", 42) // Error: String and Int not comparableWhen the compiler cannot find a suitable CanEqual instance, it may synthesize one using multiversal equality rules. This happens automatically for:
If you need to disable type-safe equality checking (for legacy code), you can import:
import scala.language.strictEquality
// Disables CanEqual checking - use with cautionThe @implicitNotFound annotation provides clear error messages:
Values of types String and Int cannot be compared with == or !=
The capability can be provided by one of the following:
- Adding a using clause `(using CanEqual[String, Int])` to the definition
- Defining a given instance of type CanEqual[String, Int]This system prevents common bugs where unrelated types are accidentally compared, while maintaining flexibility for legitimate use cases through explicit CanEqual instances.