Equality For All!

24
Equality For All! Scala by the Bay, 2014 Bill Venners Artima, Inc. Escalate Software Saturday, August 9, 2014

description

Bill Venners lightening talk about recent work on equality in Scalactic and ScalaTest from the Scala by the Bay 2014 conference.

Transcript of Equality For All!

Page 1: Equality For All!

Equality For All!Scala by the Bay, 2014

Bill VennersArtima, Inc.

Escalate Software

Saturday, August 9, 2014

Page 2: Equality For All!

Equality: An equivalence relation with one element per equivalence class

42 4341

42 = 4241 = 41 43 = 43

reflexive: x = x

symmetric: x = y iff y = x

transitive: if x = y and y = z then x = z

Saturday, August 9, 2014

Page 3: Equality For All!

How do I say forty two in code?Let me count the ways...

42

42L

42.0

42.0F

42.toShort

'*'

42.toByte BigInt(42)

BigDecimal(42)

new java.lang.Integer(42)

new java.lang.Long(42L)

new java.lang.Double(42.0)

new java.lang.Float(42.0F) new java.lang.Short(42.toShort)

new java.lang.Character(42)

new java.lang.Byte(42.toByte)

new java.math.BigInteger("42")

new java.math.BigDecimal(42) Complex(42.0, 0.0)

DigitString("42") DigitString("042")

Saturday, August 9, 2014

Page 4: Equality For All!

The equals method implements an equivalence relation on non-null object references:• It is reflexive: for any non-null reference value x, x.equals(x) should return true.• It is symmetric: for any non-null reference values x and y, x.equals(y) should return true

if and only if y.equals(x) returns true.• It is transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true

and y.equals(z) returns true, then x.equals(z) should return true.• It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.

• For any non-null reference value x, x.equals(null) should return false.

Java's equals contract

public boolean equals(Object obj)

final def == (other: Any): Boolean

Saturday, August 9, 2014

Page 5: Equality For All!

scala> 42 == 42Lres0: Boolean = true

scala> BigDecimal(42) == '*'res1: Boolean = true

scala> 42.0F == BigInt(42)res2: Boolean = true

scala> new java.lang.Float(42.0F) == 42.toShortres3: Boolean = true

Co-operative Equality Between Types

Saturday, August 9, 2014

Page 6: Equality For All!

scala> import scala.collection.mutableimport scala.collection.mutable

scala> Set(BigInt(4), BigInt(2)) == mutable.Set(4.toByte, 2.toByte)res4: Boolean = true

scala> Vector(Left(4), Right(2)) == List(Left(4L), Right(2L))res5: Boolean = true

scala> List(mutable.Set(Map(Some(4L) -> BigInt(2)))) == Vector(Set(mutable.Map(Some(4.0) -> new java.lang.Long(2L))))res6: Boolean = true

Co-operative Equality Between Types

Saturday, August 9, 2014

Page 7: Equality For All!

scala> Array(4, 2) == Array(4, 2)res7: Boolean = false

scala> <forty><two></two></forty> == <forty> <two></two> </forty>res8: Boolean = false

scala> new java.math.BigDecimal("42.0") == new java.math.BigDecimal("42.00")res9: Boolean = false

scala> 42.0 == 41.9999999999res10: Boolean = false

scala> "case" == "CASE"res11: Boolean = false

Wanted: Alternate equalities

Saturday, August 9, 2014

Page 8: Equality For All!

scala> "forty two" == 42res19: Boolean = false

scala> List(mutable.Set(Map(Some(4L) -> BigInt(2)))) == Vector(Set(mutable.Map(Some("4.0") -> new java.lang.Long(2L))))res23: Boolean = false

Wanted: Type errors

But how to decide which comparisons compile?

Saturday, August 9, 2014

Page 9: Equality For All!

scala> "forty two" == 42<console>:20: error: types String and Int do not adhere to the type constraint selected for the === and !== operators; the missing implicit parameter is of type org.scalactic.EqualityConstraint[String,Int] "forty two" === 42 ^

scala> BigInt(42) === BigDecimal(42)res1: Boolean = true

Fail to compile if L can never equal R.

Saturday, August 9, 2014

Page 10: Equality For All!

scala> Vector.empty === List.empty<console>:20: error: ambiguous implicit values: both method conflictingEmptySeqConstraint1 in object EqualityConstraint of type [LSEQ[e] <: scala.collection.GenSeq[e], RSEQ[e] <: scala.collection.GenSeq[e]]=> org.scalactic.EqualityConstraint[LSEQ[Nothing],RSEQ[Nothing]] and method conflictingEmptySeqConstraint2 in object EqualityConstraint of type [LSEQ[e] <: scala.collection.GenSeq[e], RSEQ[e] <: scala.collection.GenSeq[e]]=> org.scalactic.EqualityConstraint[LSEQ[Nothing],RSEQ[Nothing]] match expected type org.scalactic.EqualityConstraint[scala.collection.immutable.Vector[A],List[Nothing]] Vector.empty === List.empty ^

scala> Vector.empty === List.empty[Int]res3: Boolean = true

scala> Vector.empty[String] === List.emptyres4: Boolean = true

Fail to compile if L will always equal R.

Saturday, August 9, 2014

Page 11: Equality For All!

Candidate rule:

To compile, an equality comparison must be interesting: values of types L

and R can be either equal or unequal.

Saturday, August 9, 2014

Page 12: Equality For All!

scala> case class Complex(real: Double, imaginary: Double)defined class Complex

scala> implicit def convertIntToComplex(i: Int): Complex = Complex(i, 0.0)convertIntToComplex: (i: Int)Complex

scala> 42 === Complex(42, 0)<console>:24: error: types Int and Complex do not adhere to the type constraint selected for the === and !== operators; the missing implicit parameter is of type org.scalactic.EqualityConstraint[Int,Complex] 42 === Complex(42, 0) ^

scala> Complex(42, 0) === 42<console>:24: error: types Complex and Int do not adhere to the type constraint selected for the === and !== operators; the missing implicit parameter is of type org.scalactic.EqualityConstraint[Complex,Int] Complex(42, 0) === 42 ^

What about implicit conversions?

Saturday, August 9, 2014

Page 13: Equality For All!

scala> implicit val enabler = EqualityEnabledBetween[Int, Complex]enabler: org.scalactic.EqualityEnabledBetween[Int,Complex] = org.scalactic.EqualityEnabledBetween@e5d2d9b

scala> 42 === Complex(42, 0)res2: Boolean = true

scala> Complex(42, 0) === 42res3: Boolean = true

scala> new AnyShouldWrapper(1) === 1 // Probably shouldn't enable...

Intuition: enable some but not all

But what would the rule be?Saturday, August 9, 2014

Page 14: Equality For All!

OK if the conversion is an injection

scala> case class DigitString(digits: String) { | val toInt: Int = digits.toInt | }defined class DigitString

scala> implicit def convert(d: DigitString): Int = | d.digits.toIntconvertDigitStringToInt: (d: DigitString)Int

scala> DigitString("42") === DigitString("042")res0: Boolean = false

scala> DigitString("42").toInt === DigitString("042").toIntres1: Boolean = true

John C. Reynolds: Using category theory to design implicit conversions and generic operators.

Saturday, August 9, 2014

Page 15: Equality For All!

How to decide:

1. To compile, an equality comparison must be interesting: values of types L and R can be either equal or unequal.

2. Allow select implicit conversions to be enabled, and recommend that non-widening conversions (non-injections) not be enabled.

Saturday, August 9, 2014

Page 16: Equality For All!

scala> (Some(1): Option[Int]) === Some(1)res0: Boolean = true

scala> Some(1) === (Some(1): Option[Int])res1: Boolean = true

scala> Some(1) === Some(1)res2: Boolean = true

What about the implicit conversion from subtype to supertype (<:<)?

Saturday, August 9, 2014

Page 17: Equality For All!

scala> def eqv[T](a: T, b: T): Boolean = a === beqv: [T](a: T, b: T)Boolean

scala> eqv(1, ())res3: Boolean = false

scala> ((i: Int) => i + 1) === ((i: Int) => i + 1)res4: Boolean = false

Even though <:< is an injection, is it always desireable?

Saturday, August 9, 2014

Page 18: Equality For All!

EqualityPolicy

UncheckedEquality CheckedEquality EnabledEquality

Saturday, August 9, 2014

Page 19: Equality For All!

scala> import EnabledEquality._import EnabledEquality._

scala> def eqv[T](a: T, b: T): Boolean = a === b<console>:19: error: types T and T do not adhere to the type constraint selected for the === and !== operators; the missing implicit parameter is of type org.scalactic.EqualityConstraint[T,T] def eqv[T](a: T, b: T): Boolean = a === b ^

scala> ((i: Int) => i + 1) === ((i: Int) => i + 1)<console>:20: error: types Int => Int and Int => Int do not adhere to the type constraint selected for the === and !== operators; the missing implicit parameter is of type org.scalactic.EqualityConstraint[Int => Int,Int => Int] ((i: Int) => i + 1) === ((i: Int) => i + 1) ^

EnabledEquality benefit

Saturday, August 9, 2014

Page 20: Equality For All!

scala> case class Person(name: String)defined class Person

scala> Person("Sue") === Person("Sue")<console>:22: error: types Person and Person do not adhere to the type constraint selected for the === and !== operators; the missing implicit parameter is of type org.scalactic.EqualityConstraint[Person,Person] Person("Sue") === Person("Sue") ^

scala> implicit val enabler = new EqualityEnabledFor[Person]enabler: org.scalactic.EqualityEnabledFor[Person] = org.scalactic.EqualityEnabledFor@1289d391

scala> Person("Sue") === Person("Sue")res2: Boolean = true

EnabledEquality cost

Saturday, August 9, 2014

Page 21: Equality For All!

scala> 1 should === ("one")<console>:23: error: types Int and String do not adhere to the type constraint selected for the === and !== operators; the missing implicit parameter is of type org.scalactic.Constraint[Int,String] 1 should === ("one") ^

scala> 1 should equal ("one")<console>:23: error: could not find implicit value for parameter typeClass1: org.scalactic.enablers.EvidenceThat[String]#CanEqual[Int] 1 should equal ("one") ^

scala> 1 should be ("one")<console>:23: error: could not find implicit value for parameter typeClass1: org.scalactic.enablers.EvidenceThat[String]#CanEqual[Int] 1 should be ("one") ^

scala> 1 should be_== ("one")org.scalatest.exceptions.TestFailedException: 1 was not equal to "one"

ScalaTest's equal, be, and be_==

Saturday, August 9, 2014

Page 22: Equality For All!

scala> List(1, 2, 3) should contain ("one")<console>:23: error: could not find implicit value for parameter typeClass1: org.scalactic.enablers.EvidenceThat[String]#CanBeContainedIn[List[Int]] List(1, 2, 3) should contain ("one") ^

scala> List(1, 2, 3) should contain oneOf ("one", "two")<console>:23: error: could not find implicit value for parameter evidence: org.scalactic.enablers.EvidenceThat[String]#CanBeContainedIn[List[Int]] List(1, 2, 3) should contain oneOf("one", "two") ^

scala> 1 isIn List(1, 2, 3)res14: Boolean = true

scala> "one" isIn List(1, 2, 3)<console>:23: error: Could not find evidence that String can be contained in List[Int]; the missing implicit parameter is of type org.scalactic.enablers.ContainingConstraint[List[Int],String] "one" isIn List(1, 2, 3) ^

ScalaTest's contain, Scalactic's isIn/isNotIn

Saturday, August 9, 2014

Page 23: Equality For All!

if sufficientTimeRemains then (Q => A) else thanks

artima.com/shop

15% discountcoupon code:

BYTHEBAY2014

Saturday, August 9, 2014

Page 24: Equality For All!

scala> 1L === 1 res0: Boolean = true

scala> 1 === 1L<console>:14: error: could not find implicit value for parameter F0: scalaz.Equal[Any] 1 === 1L ^

Scalaz or Spire

Scalacticscala> 1L === 1res7: Boolean = true

scala> 1 === 1Lres8: Boolean = true

Equal[T], Eq[T]

EqualityConstraint[L, R]

Equivalence[T]

Saturday, August 9, 2014