or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

annotations.mdcompile-time.mdgeneric-programming.mdimmutable-arrays.mdindex.mdmacros.mdstructural-types.mdtuples.mdtype-safe-equality.md
tile.json

type-safe-equality.mddocs/

Type-Safe Equality

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.

Core API

CanEqual Trait

@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.

Universal Instances

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 required
  • canEqualAny: Fallback method to create equality instances for any types

Built-in Type Instances

given canEqualNumber: CanEqual[Number, Number]
given canEqualString: CanEqual[String, String]

Pre-defined instances for common Java types that can be safely compared.

Collection Type Instances

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.

Usage Examples

Basic Type Safety

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 instances

Collection Comparisons

val 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]

Custom Type Instances

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)

Pattern Matching Support

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"

Working with Generic Code

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 comparable

Advanced Patterns

Multiversal Equality

When the compiler cannot find a suitable CanEqual instance, it may synthesize one using multiversal equality rules. This happens automatically for:

  • Primitive types (Int, Double, Boolean, etc.)
  • Types that share a common supertype in certain circumstances
  • null comparisons

Disabling Type-Safe Equality

If you need to disable type-safe equality checking (for legacy code), you can import:

import scala.language.strictEquality
// Disables CanEqual checking - use with caution

Error Messages

The @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.