From 2e42e6a1a5328ea3ce186827c0bcfdebc24cea9c Mon Sep 17 00:00:00 2001 From: Georgi Krastev Date: Sun, 10 Oct 2021 22:00:33 +0200 Subject: [PATCH] Port Eq and Hash plus tests --- .../main/scala-3/cats/derived/DerivedEq.scala | 31 +++++++++ .../scala-3/cats/derived/DerivedHash.scala | 45 +++++++++++++ core/src/main/scala-3/cats/derived/eq.scala | 30 --------- core/src/main/scala-3/cats/derived/hash.scala | 38 ----------- .../main/scala-3/cats/derived/package.scala | 14 +++- .../test/scala-3/cats/derived/EqSuite.scala | 66 +++++++++++++++++++ .../test/scala-3/cats/derived/EqTests.scala | 2 +- .../test/scala-3/cats/derived/HashSuite.scala | 50 ++++++++++++++ .../test/scala-3/cats/derived/HashTests.scala | 2 +- .../scala-3/cats/derived/KittensSuite.scala | 36 +++++----- .../test/scala-3/cats/derived/adtdefns.scala | 6 ++ 11 files changed, 230 insertions(+), 90 deletions(-) create mode 100644 core/src/main/scala-3/cats/derived/DerivedEq.scala create mode 100644 core/src/main/scala-3/cats/derived/DerivedHash.scala delete mode 100644 core/src/main/scala-3/cats/derived/eq.scala delete mode 100644 core/src/main/scala-3/cats/derived/hash.scala create mode 100644 core/src/test/scala-3/cats/derived/EqSuite.scala create mode 100644 core/src/test/scala-3/cats/derived/HashSuite.scala diff --git a/core/src/main/scala-3/cats/derived/DerivedEq.scala b/core/src/main/scala-3/cats/derived/DerivedEq.scala new file mode 100644 index 00000000..0a0cee06 --- /dev/null +++ b/core/src/main/scala-3/cats/derived/DerivedEq.scala @@ -0,0 +1,31 @@ +package cats.derived + +import cats.Eq +import shapeless3.deriving.{Complete, K0} + +import scala.compiletime.* + +type DerivedEq[A] = Derived[Eq[A]] +object DerivedEq: + type Or[A] = Derived.Or[Eq[A]] + inline def apply[A]: Eq[A] = + import DerivedEq.given + summonInline[DerivedEq[A]].instance + + given [A](using inst: K0.ProductInstances[Or, A]): DerivedEq[A] = + given K0.ProductInstances[Eq, A] = inst.unify + new Product[Eq, A] {} + + given [A](using inst: => K0.CoproductInstances[Or, A]): DerivedEq[A] = + given K0.CoproductInstances[Eq, A] = inst.unify + new Coproduct[Eq, A] {} + + trait Product[F[x] <: Eq[x], A](using inst: K0.ProductInstances[F, A]) extends Eq[A]: + def eqv(x: A, y: A): Boolean = inst.foldLeft2(x, y)(true: Boolean)( + [t] => (acc: Boolean, eqt: F[t], x: t, y: t) => Complete(!eqt.eqv(x, y))(false)(true) + ) + + trait Coproduct[F[x] <: Eq[x], A](using inst: K0.CoproductInstances[F, A]) extends Eq[A]: + def eqv(x: A, y: A): Boolean = inst.fold2(x, y)(false)( + [t] => (eqt: F[t], x: t, y: t) => eqt.eqv(x, y) + ) diff --git a/core/src/main/scala-3/cats/derived/DerivedHash.scala b/core/src/main/scala-3/cats/derived/DerivedHash.scala new file mode 100644 index 00000000..6fe2a028 --- /dev/null +++ b/core/src/main/scala-3/cats/derived/DerivedHash.scala @@ -0,0 +1,45 @@ +package cats.derived + +import cats.Hash +import shapeless3.deriving.{K0, Continue} + +import scala.compiletime.* +import scala.util.hashing.MurmurHash3 + +type DerivedHash[A] = Derived[Hash[A]] +object DerivedHash: + type Or[A] = Derived.Or[Hash[A]] + inline def apply[A]: Hash[A] = + import DerivedHash.given + summonInline[DerivedHash[A]].instance + + given [A <: scala.Product](using inst: K0.ProductInstances[Or, A]): DerivedHash[A] = + given K0.ProductInstances[Hash, A] = inst.unify + new Product[Hash, A] {} + + given [A](using inst: => K0.CoproductInstances[Or, A]): DerivedHash[A] = + given K0.CoproductInstances[Hash, A] = inst.unify + new Coproduct[Hash, A] {} + + trait Product[F[x] <: Hash[x], A <: scala.Product](using inst: K0.ProductInstances[F, A]) + extends DerivedEq.Product[F, A], + Hash[A]: + + def hash(x: A): Int = + val arity = x.productArity + val prefix = x.productPrefix.hashCode + if arity <= 0 then prefix + else + MurmurHash3.finalizeHash( + inst.foldLeft[Int](x)(MurmurHash3.mix(MurmurHash3.productSeed, prefix))( + [t] => (acc: Int, h: F[t], x: t) => Continue(MurmurHash3.mix(acc, h.hash(x))) + ), + arity + ) + + trait Coproduct[F[x] <: Hash[x], A](using inst: K0.CoproductInstances[F, A]) + extends DerivedEq.Coproduct[F, A], + Hash[A]: + + def hash(x: A): Int = + inst.fold[Int](x)([t] => (h: F[t], x: t) => h.hash(x)) diff --git a/core/src/main/scala-3/cats/derived/eq.scala b/core/src/main/scala-3/cats/derived/eq.scala deleted file mode 100644 index 496eefae..00000000 --- a/core/src/main/scala-3/cats/derived/eq.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cats.derived - -import cats.Eq -import shapeless3.deriving.{K0, Complete} - -trait ProductEq[T[x] <: Eq[x], A](using inst: K0.ProductInstances[T, A]) extends Eq[A]: - - def eqv(x: A, y: A): Boolean = - inst.foldLeft2(x, y)(true: Boolean)( - [t] => (acc: Boolean, eqt: T[t], t0: t, t1: t) => Complete(!eqt.eqv(t0, t1))(false)(true) - ) - -trait CoproductEq[T[x] <: Eq[x], A](using inst: K0.CoproductInstances[T, A]) extends Eq[A]: - - def eqv(x: A, y: A): Boolean = - inst.fold2(x, y)(false)( - [t] => (eqt: T[t], t0: t, t1: t) => eqt.eqv(t0, t1) - ) - - -trait EqDerivation: - extension (F: Eq.type) - inline def derived[A](using gen: K0.Generic[A]): Eq[A] = - gen.derive(productEq, coproductEq) - - given productEq[A](using K0.ProductInstances[Eq, A]): Eq[A] = - new ProductEq[Eq, A]{} - - given coproductEq[A](using K0.CoproductInstances[Eq, A]): Eq[A] = - new CoproductEq[Eq, A]{} diff --git a/core/src/main/scala-3/cats/derived/hash.scala b/core/src/main/scala-3/cats/derived/hash.scala deleted file mode 100644 index 39c1999b..00000000 --- a/core/src/main/scala-3/cats/derived/hash.scala +++ /dev/null @@ -1,38 +0,0 @@ -package cats.derived - -import cats.Hash -import shapeless3.deriving.{K0, Continue} - -import scala.compiletime.summonInline -import scala.util.hashing.MurmurHash3 - -trait ProductHash[T[x] <: Hash[x], A](using inst: K0.ProductInstances[T, A], ev: A <:< Product) - extends ProductEq[T, A], Hash[A]: - - def hash(x: A): Int = { - val hash = inst.foldLeft[Int](x)((MurmurHash3.productSeed))( - [t] => (acc: Int, h: T[t], t0: t) => Continue(MurmurHash3.mix(acc, h.hash(t0))) - ) - MurmurHash3.finalizeHash(hash, ev(x).productArity) - } - -trait CoproductHash[T[x] <: Hash[x], A](using inst: K0.CoproductInstances[T, A]) - extends CoproductEq[T, A], Hash[A]: - - def hash(x: A): Int = inst.fold[Int](x)( - [t] => (h: T[t], t0: t) => h.hash(t0) - ) - -trait HashDerivation: - extension (F: Hash.type) - inline def derived[A](using gen: K0.Generic[A]): Hash[A] = - inline gen match { - case p: K0.ProductGeneric[A] => productHash(using p.asInstanceOf, summonInline[A <:< Product]) - case c: K0.CoproductGeneric[A] => coproductHash(using c.asInstanceOf) - } - - given productHash[A](using K0.ProductInstances[Hash, A], A <:< Product): Hash[A] = - new ProductHash[Hash, A]{} - - given coproductHash[A](using K0.CoproductInstances[Hash, A]): Hash[A] = - new CoproductHash[Hash, A]{} diff --git a/core/src/main/scala-3/cats/derived/package.scala b/core/src/main/scala-3/cats/derived/package.scala index ce4d8ca8..85ed14f7 100644 --- a/core/src/main/scala-3/cats/derived/package.scala +++ b/core/src/main/scala-3/cats/derived/package.scala @@ -6,6 +6,8 @@ import cats.kernel.{CommutativeSemigroup, CommutativeMonoid} import scala.util.NotGiven +extension (E: Eq.type) inline def derived[A]: Eq[A] = DerivedEq[A] +extension (H: Hash.type) inline def derived[A]: Hash[A] = DerivedHash[A] extension (E: Empty.type) inline def derived[A]: Empty[A] = DerivedEmpty[A] extension (S: Semigroup.type) inline def derived[A]: Semigroup[A] = DerivedSemigroup[A] extension (M: Monoid.type) inline def derived[A]: Monoid[A] = DerivedMonoid[A] @@ -19,8 +21,6 @@ extension (F: Traverse.type) inline def derived[F[_]]: Traverse[F] = DerivedTrav object semiauto extends ContravariantDerivation, EmptyKDerivation, - EqDerivation, - HashDerivation, InvariantDerivation, MonoidKDerivation, OrderDerivation, @@ -29,6 +29,8 @@ object semiauto ShowDerivation, Instances: + inline def eq[A]: Eq[A] = DerivedEq[A] + inline def hash[A]: Hash[A] = DerivedHash[A] inline def empty[A]: Empty[A] = DerivedEmpty[A] inline def semigroup[A]: Semigroup[A] = DerivedSemigroup[A] inline def monoid[A]: Monoid[A] = DerivedMonoid[A] @@ -40,6 +42,12 @@ object semiauto inline def traverse[F[_]]: Traverse[F] = DerivedTraverse[F] object auto: + object eq: + inline given [A](using NotGiven[Eq[A]]): Eq[A] = DerivedEq[A] + + object hash: + inline given [A](using NotGiven[Hash[A]]): Hash[A] = DerivedHash[A] + object empty: inline given [A](using NotGiven[Empty[A]]): Empty[A] = DerivedEmpty[A] @@ -54,7 +62,7 @@ object auto: object commutativeMonoid: inline given [A](using NotGiven[CommutativeMonoid[A]]): CommutativeMonoid[A] = DerivedCommutativeMonoid[A] - + object functor: inline given [F[_]](using NotGiven[Functor[F]]): Functor[F] = DerivedFunctor[F] diff --git a/core/src/test/scala-3/cats/derived/EqSuite.scala b/core/src/test/scala-3/cats/derived/EqSuite.scala new file mode 100644 index 00000000..427319e0 --- /dev/null +++ b/core/src/test/scala-3/cats/derived/EqSuite.scala @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2015 Miles Sabin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.derived + +import cats.Eq +import cats.kernel.laws.discipline.{EqTests, SerializableTests} + +import scala.compiletime.* + +class EqSuite extends KittensSuite.WithoutEq: + import EqSuite.* + import TestDefns.* + + inline def eqTests[A]: EqTests[A] = + EqTests[A](summonInline) + + inline def testEq(inline context: String): Unit = + checkAll(s"$context.Eq[Foo]]", eqTests[Foo].eqv) + checkAll(s"$context.Eq[IList[Int]]", eqTests[IList[Int]].eqv) + checkAll(s"$context.Eq[Inner]", eqTests[Inner].eqv) + checkAll(s"$context.Eq[Outer]", eqTests[Outer].eqv) + checkAll(s"$context.Eq[Interleaved[Int]]", eqTests[Interleaved[Int]].eqv) + checkAll(s"$context.Eq[Tree[Int]]", eqTests[Tree[Int]].eqv) + // FIXME: Doesn't work for recursive case classes. + // checkAll(s"$context.Eq[Recursive]", eqTests[Recursive].eqv) + checkAll(s"$context.Eq is Serializable", SerializableTests.serializable(summonInline[Eq[Foo]])) + + locally { + import auto.eq.given + testEq("auto") + } + + locally { + import semiInstances.given + testEq("semiauto") + } + +end EqSuite + +object EqSuite: + import TestDefns.* + + object semiInstances: + given Eq[Foo] = semiauto.eq + given Eq[IList[Int]] = semiauto.eq + given Eq[Inner] = semiauto.eq + given Eq[Outer] = semiauto.eq + given Eq[Interleaved[Int]] = semiauto.eq + given Eq[Tree[Int]] = semiauto.eq + given Eq[Recursive] = semiauto.eq + +end EqSuite diff --git a/core/src/test/scala-3/cats/derived/EqTests.scala b/core/src/test/scala-3/cats/derived/EqTests.scala index 39175c4c..b9a128ce 100644 --- a/core/src/test/scala-3/cats/derived/EqTests.scala +++ b/core/src/test/scala-3/cats/derived/EqTests.scala @@ -2,7 +2,7 @@ package cats.derived import alleycats.* import cats.* -import cats.derived.semiauto.* +import cats.derived.* class EqTests { // case class Foo(i: Int, b: Option[String]) derives Eq diff --git a/core/src/test/scala-3/cats/derived/HashSuite.scala b/core/src/test/scala-3/cats/derived/HashSuite.scala new file mode 100644 index 00000000..31bc5ffe --- /dev/null +++ b/core/src/test/scala-3/cats/derived/HashSuite.scala @@ -0,0 +1,50 @@ +package cats.derived + +import cats.Hash +import cats.kernel.laws.discipline.{HashTests, SerializableTests} + +import scala.compiletime.* +import scala.util.hashing.MurmurHash3 + +class HashSuite extends KittensSuite: + import HashSuite.* + import TestDefns.* + + inline def hashTests[A]: HashTests[A] = + HashTests[A](summonInline) + + inline def testHash(inline context: String): Unit = + checkAll(s"$context.Hash[IList[Int]]", hashTests[IList[Int]].hash) + checkAll(s"$context.Hash[Inner]", hashTests[Inner].hash) + checkAll(s"$context.Hash[Outer]", hashTests[Outer].hash) + // FIXME: typelevel/cats#2878 + // checkAll(s"$context.Hash[Interleaved[Int]]", hashTests[Interleaved[Int]].hash) + checkAll(s"$context.Hash[Tree[Int]]", hashTests[Tree[Int]].hash) + // FIXME: Doesn't work for recursive case classes. + // checkAll(s"$context.Hash[Recursive]", hashTests[Recursive].hash) + checkAll(s"$context.Hash is Serializable", SerializableTests.serializable(summonInline[Hash[Inner]])) + + locally { + import auto.hash.given + testHash("auto") + } + + locally { + import semiInstances.given + testHash("semiauto") + } + +end HashSuite + +object HashSuite: + import TestDefns.* + + object semiInstances: + given Hash[IList[Int]] = semiauto.hash + given Hash[Inner] = semiauto.hash + given Hash[Outer] = semiauto.hash + given Hash[Interleaved[Int]] = semiauto.hash + given Hash[Tree[Int]] = semiauto.hash + given Hash[Recursive] = semiauto.hash + +end HashSuite diff --git a/core/src/test/scala-3/cats/derived/HashTests.scala b/core/src/test/scala-3/cats/derived/HashTests.scala index d93a793f..49de408d 100644 --- a/core/src/test/scala-3/cats/derived/HashTests.scala +++ b/core/src/test/scala-3/cats/derived/HashTests.scala @@ -2,7 +2,7 @@ package cats.derived import alleycats.* import cats.* -import cats.derived.semiauto.* +import cats.derived.* class HashTests { // case class Foo(i: Int, b: Option[String]) derives Hash diff --git a/core/src/test/scala-3/cats/derived/KittensSuite.scala b/core/src/test/scala-3/cats/derived/KittensSuite.scala index 4103bd4d..33e8133c 100644 --- a/core/src/test/scala-3/cats/derived/KittensSuite.scala +++ b/core/src/test/scala-3/cats/derived/KittensSuite.scala @@ -27,23 +27,25 @@ import scala.quoted.* /** An opinionated stack of traits to improve consistency and reduce boilerplate in Kittens tests. Note that unlike the * corresponding CatsSuite in the Cat project, this trait does not mix in any instances. */ -abstract class KittensSuite extends DisciplineSuite, AllSyntax, TestEqInstances: - override val scalaCheckTestParameters: Parameters = super.scalaCheckTestParameters - .withMinSuccessfulTests(if (Platform.isJvm) 50 else 5) - .withMaxDiscardRatio(if (Platform.isJvm) 5 else 50) - .withWorkers(if (Platform.isJvm) 2 else 1) - .withMaxSize(if (Platform.isJvm) 10 else 5) - .withMinSize(0) - - given [A: Arbitrary]: Arbitrary[List[A]] = - Arbitrary.arbContainer - - inline def nameOf[A]: String = - ${ KittensSuite.nameOfMacro[A] } - - inline def testNoInstance(inline code: String, message: String): Unit = - test(s"No $code")(assert(compileErrors(code).contains(message))) - +abstract class KittensSuite extends KittensSuite.WithoutEq, TestEqInstances object KittensSuite: def nameOfMacro[A: Type](using Quotes) = Expr(Type.show[A]) + + /** Used to test `Eq` derivation. */ + abstract class WithoutEq extends DisciplineSuite, AllSyntax: + override val scalaCheckTestParameters: Parameters = super.scalaCheckTestParameters + .withMinSuccessfulTests(if (Platform.isJvm) 50 else 5) + .withMaxDiscardRatio(if (Platform.isJvm) 5 else 50) + .withWorkers(if (Platform.isJvm) 2 else 1) + .withMaxSize(if (Platform.isJvm) 10 else 5) + .withMinSize(0) + + given [A: Arbitrary]: Arbitrary[List[A]] = + Arbitrary.arbContainer + + inline def nameOf[A]: String = + ${ KittensSuite.nameOfMacro[A] } + + inline def testNoInstance(inline code: String, message: String): Unit = + test(s"No $code")(assert(compileErrors(code).contains(message))) diff --git a/core/src/test/scala-3/cats/derived/adtdefns.scala b/core/src/test/scala-3/cats/derived/adtdefns.scala index 7f5d171f..69409cc8 100644 --- a/core/src/test/scala-3/cats/derived/adtdefns.scala +++ b/core/src/test/scala-3/cats/derived/adtdefns.scala @@ -471,4 +471,10 @@ trait TestEqInstances { implicit def eqCaseClassWOption[A: Eq]: Eq[CaseClassWOption[A]] = Eq.by(_.value) + + implicit val eqInner: Eq[Inner] = + Eq.fromUniversalEquals + + implicit val eqOuter: Eq[Outer] = + Eq.fromUniversalEquals }