Last active
October 6, 2018 18:56
-
-
Save odersky/2d5110ac4c14a801eaf0 to your computer and use it in GitHub Desktop.
Better equality for Scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import annotation.unchecked.uncheckedVariance | |
/** The trait of things that can be compared safely */ | |
trait Equals[-T] { | |
/** A witness of Equals' type parameter. Should only used for | |
* the constraint in EqlDecorator, hence, @uncheckedVariance should not be a problem. | |
*/ | |
type EqualsDomain = T @uncheckedVariance | |
/** The safe equals operation forwards to universal equals. */ | |
def safeEquals(that: T): Boolean = this.equals(that) | |
} | |
/** Decorator classes; these could become part of Predef */ | |
object decorators { | |
/** The decorator that provides type-safe equality to classes that extend Equals */ | |
implicit class EqlDecorator[T <: Equals[_]](self: T) { | |
def === [U >: T <: self.EqualsDomain](other: U) = | |
self.safeEquals(other.asInstanceOf) | |
} | |
/** A fallback that makes === universal for classes that do not implement Equals. */ | |
implicit class AnyEql(self: Any) { | |
def === (other: Any) = self.equals(other) | |
} | |
/** An alternative fallback that forces ambiguous implicits when universal equality | |
* is selected for classes that do implement Equals. | |
*/ | |
implicit class AnyEql2(self: Any) { | |
def === (other: Equals[_]) = self.equals(other) | |
} | |
} | |
object eqls extends App { | |
import decorators._ | |
// A simple class that opts into type-safe equality | |
class C extends Equals[C] | |
val c = new C | |
println(c === c) | |
println("abc" === 1) | |
println(c === 1) // error | |
println(1 === c) // error | |
// A generic class that admits type-safe equlity only when compared against | |
// the same generic instance. | |
class Option[T] extends Equals[Option[T]] | |
case class Some[T](x: T) extends Option[T] | |
def some[T](x: T): Option[T] = Some(x) | |
class Fruit extends Equals[Fruit] | |
val apple, pear = new Fruit | |
class Instrument extends Equals[Instrument] | |
val banjo = new Instrument | |
some(apple) === some(pear) | |
Some(apple) === Some(pear) | |
some(apple) === some(banjo) // error | |
Some(apple) === Some(banjo) // error | |
some(apple) === 1 // error | |
"abc" === some(banjo) // error | |
Some(apple) === 1 // error | |
"abc" === Some(apple) // error | |
// A class that does not admit comparison at all | |
class NE extends Equals[Nothing] | |
val ne = new NE | |
ne === ne // error | |
// Equality between subclass and superclass members | |
trait T2 extends Equals[T2] | |
final class C2 extends T2 | |
final class C3 extends T2 | |
val t2: T2 = new C2 | |
val c2: C2 = new C2 | |
val c3: C3 = new C3 | |
c2 === t2 | |
t2 === c2 | |
c2 === c3 | |
// Making other operations safe-equality-aware. | |
def distinct[T <: Equals[T]](xs: List[T]): List[T] = ??? | |
distinct(List(apple, pear)) | |
distinct(List(Some(apple), Some(pear))) | |
distinct(List(c2, c3)) | |
distinct(List(c2, c2)) | |
distinct(List(1, "abc")) // error | |
distinct(List(Some(apple), Some(banjo))) // error | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment