From 399c14c4b6897b86ad732d0f03778f2b9185128c Mon Sep 17 00:00:00 2001 From: Cody Allen Date: Tue, 8 Jan 2019 06:55:01 -0800 Subject: [PATCH] Override equals and hashCode on Chain Resolves #2540. --- core/src/main/scala/cats/data/Chain.scala | 20 ++++++++++- .../main/scala/cats/tests/ListWrapper.scala | 2 ++ .../test/scala/cats/tests/ChainSuite.scala | 34 ++++++++++++++++++- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/cats/data/Chain.scala b/core/src/main/scala/cats/data/Chain.scala index ba94485b8a..aa229dc9ba 100644 --- a/core/src/main/scala/cats/data/Chain.scala +++ b/core/src/main/scala/cats/data/Chain.scala @@ -2,6 +2,7 @@ package cats package data import Chain._ +import cats.kernel.instances.StaticMethods import scala.annotation.tailrec import scala.collection.immutable.SortedMap @@ -394,7 +395,16 @@ sealed abstract class Chain[+A] { builder.result } + def hash[AA >: A](implicit hashA: Hash[AA]): Int = StaticMethods.orderedHash((this: Chain[AA]).iterator) + override def toString: String = show(Show.show[A](_.toString)) + + override def equals(o: Any): Boolean = + if (o.isInstanceOf[Chain[_]]) + (this: Chain[Any]).===(o.asInstanceOf[Chain[Any]])(Eq.fromUniversalEquals[Any]) + else false + + override def hashCode: Int = hash(Hash.fromUniversalHashCode[A]) } object Chain extends ChainInstances { @@ -646,7 +656,15 @@ sealed abstract private[data] class ChainInstances1 extends ChainInstances2 { new ChainPartialOrder[A] { implicit def A: PartialOrder[A] = A0 } } -sealed abstract private[data] class ChainInstances2 { +sealed abstract private[data] class ChainInstances2 extends ChainInstances3 { + implicit def catsDataHashForChain[A](implicit A: Hash[A]): Hash[Chain[A]] = new Hash[Chain[A]] { + def eqv(x: Chain[A], y: Chain[A]): Boolean = x === y + + def hash(fa: Chain[A]): Int = fa.hash + } +} + +sealed abstract private[data] class ChainInstances3 { implicit def catsDataEqForChain[A](implicit A: Eq[A]): Eq[Chain[A]] = new Eq[Chain[A]] { def eqv(x: Chain[A], y: Chain[A]): Boolean = x === y } diff --git a/testkit/src/main/scala/cats/tests/ListWrapper.scala b/testkit/src/main/scala/cats/tests/ListWrapper.scala index 009b3d0c7f..7799435878 100644 --- a/testkit/src/main/scala/cats/tests/ListWrapper.scala +++ b/testkit/src/main/scala/cats/tests/ListWrapper.scala @@ -41,6 +41,8 @@ object ListWrapper { def eqv[A: Eq]: Eq[ListWrapper[A]] = Eq.by(_.list) + def hash[A: Hash]: Hash[ListWrapper[A]] = Hash.by(_.list) + val traverse: Traverse[ListWrapper] = { val F = Traverse[List] diff --git a/tests/src/test/scala/cats/tests/ChainSuite.scala b/tests/src/test/scala/cats/tests/ChainSuite.scala index ca7f50970f..c6069efd8e 100644 --- a/tests/src/test/scala/cats/tests/ChainSuite.scala +++ b/tests/src/test/scala/cats/tests/ChainSuite.scala @@ -10,7 +10,7 @@ import cats.laws.discipline.{ TraverseFilterTests, TraverseTests } -import cats.kernel.laws.discipline.{EqTests, MonoidTests, OrderTests, PartialOrderTests} +import cats.kernel.laws.discipline.{EqTests, HashTests, MonoidTests, OrderTests, PartialOrderTests} import cats.laws.discipline.arbitrary._ class ChainSuite extends CatsSuite { @@ -48,6 +48,12 @@ class ChainSuite extends CatsSuite { checkAll("Eq[Chain[ListWrapper[Int]]", SerializableTests.serializable(Eq[Chain[ListWrapper[Int]]])) } + { + implicit val hash = ListWrapper.hash[Int] + checkAll("Chain[ListWrapper[Int]]", HashTests[Chain[ListWrapper[Int]]].hash) + checkAll("Hash[Chain[ListWrapper[Int]]", SerializableTests.serializable(Hash[Chain[ListWrapper[Int]]])) + } + test("show") { Show[Chain[Int]].show(Chain(1, 2, 3)) should ===("Chain(1, 2, 3)") Chain.empty[Int].show should ===("Chain()") @@ -186,4 +192,30 @@ class ChainSuite extends CatsSuite { a.distinct.toList should ===(a.toList.distinct) } } + + test("=== is consistent with == (issue #2540)") { + (Chain.one(1) |+| Chain.one(2) |+| Chain.one(3)) should be(Chain.fromSeq(List(1, 2, 3))) + + forAll { (a: Chain[Int], b: Chain[Int]) => + (a === b) should ===(a == b) + } + } + + test("== returns false for non-Chains") { + forAll { (a: Chain[Int], b: Int) => + (a == b) should ===(false) + } + } + + test("== returns false for Chains of different element types") { + forAll { (a: Chain[Option[String]], b: Chain[String]) => + (a == b) should ===(a.isEmpty && b.isEmpty) + } + } + + test("Chain#hashCode is consistent with List#hashCode") { + forAll { (x: Chain[Int]) => + x.hashCode should ===(x.toList.hashCode) + } + } }