Skip to content

Instantly share code, notes, and snippets.

@odersky
Last active October 6, 2018 18:56
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save odersky/2d5110ac4c14a801eaf0 to your computer and use it in GitHub Desktop.
Save odersky/2d5110ac4c14a801eaf0 to your computer and use it in GitHub Desktop.
Better equality for Scala
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