From 28d601c4a0ffd035affc420e0af0537500dac01e Mon Sep 17 00:00:00 2001 From: ismat Date: Fri, 24 Apr 2026 00:54:04 +0300 Subject: [PATCH 1/3] Fix stack overflow in derived Functor/Contravariant for recursive ADTs --- .../cats/derived/DerivedContravariant.scala | 42 +++++++++++++++---- .../scala-3/cats/derived/DerivedFunctor.scala | 42 +++++++++++++++---- .../cats/derived/ContravariantSuite.scala | 14 +++++++ .../scala-3/cats/derived/FunctorSuite.scala | 14 +++++++ 4 files changed, 98 insertions(+), 14 deletions(-) diff --git a/core/src/main/scala-3/cats/derived/DerivedContravariant.scala b/core/src/main/scala-3/cats/derived/DerivedContravariant.scala index 4c0a45cc..44b529d2 100644 --- a/core/src/main/scala-3/cats/derived/DerivedContravariant.scala +++ b/core/src/main/scala-3/cats/derived/DerivedContravariant.scala @@ -1,6 +1,6 @@ package cats.derived -import cats.{Contravariant, Functor} +import cats.{Contravariant, Eval, Functor} import shapeless3.deriving.{Const, Derived} import shapeless3.deriving.K1.* @@ -34,21 +34,49 @@ object DerivedContravariant: new Lazy(() => F.unify.composeContravariant(using G.unify)) with Contravariant[F <<< G]: export delegate.* - given [F[_]](using inst: => Instances[Contravariant |: Derived, F]): DerivedContravariant[F] = - generic(using inst.unify) + given [F[_]](using inst: ProductInstances[Contravariant |: Derived, F]): DerivedContravariant[F] = + Strict.product(using inst.unify) + + given [F[_]](using => CoproductInstances[Contravariant |: Derived, F]): DerivedContravariant[F] = + Strict.coproduct @deprecated("Kept for binary compatibility", "3.2.0") protected given [F[_]: Functor |: Derived, G[_]: Contravariant |: Derived]: DerivedContravariant[[x] =>> F[G[x]]] = nested - private def generic[F[_]: InstancesOf[Contravariant]]: DerivedContravariant[F] = - new Generic[Contravariant, F] {} + private[derived] trait Safe[F[_]] extends Contravariant[F]: + private[derived] def safeContramap[A, B](fa: F[A])(f: B => A): Eval[F[B]] + final override def contramap[A, B](fa: F[A])(f: B => A): F[B] = safeContramap(fa)(f).value + + private[derived] def safeContramap[F[_], A, B](F: Contravariant[F])(fa: F[A])(f: B => A): Eval[F[B]] = + F match + case safe: Safe[F] @scala.unchecked => safe.safeContramap(fa)(f) + case _ => Eval.later(F.contramap(fa)(f)) trait Generic[T[f[_]] <: Contravariant[f], F[_]](using inst: Instances[T, F]) extends Contravariant[F]: final override def contramap[A, B](fa: F[A])(f: B => A): F[B] = inst.map(fa)([f[_]] => (T: T[f], fa: f[A]) => T.contramap(fa)(f)) + trait Product[T[f[_]] <: Contravariant[f], F[_]](using inst: ProductInstances[T, F]) extends Safe[F]: + private[derived] final override def safeContramap[A, B](fa: F[A])(f: B => A): Eval[F[B]] = + val pure = [a] => (x: a) => Eval.now(x) + val mp = [a, b] => (ea: Eval[a], g: a => b) => ea.map(g) + val ap = [a, b] => (ef: Eval[a => b], ea: Eval[a]) => ef.flatMap(g => ea.map(g)) + inst.traverse[A, Eval, B](fa)(mp)(pure)(ap)( + [f[_]] => (F: T[f], fa: f[A]) => DerivedContravariant.safeContramap(F)(fa)(f) + ) + + trait Coproduct[T[f[_]] <: Contravariant[f], F[_]](using inst: CoproductInstances[T, F]) extends Safe[F]: + private[derived] final override def safeContramap[A, B](fa: F[A])(f: B => A): Eval[F[B]] = + Eval.defer(inst.fold(fa)( + [f[a] <: F[a]] => (F: T[f], fa: f[A]) => + DerivedContravariant.safeContramap(F)(fa)(f).asInstanceOf[Eval[F[B]]] + )) + object Strict: - given product[F[_]: ProductInstancesOf[Contravariant]]: DerivedContravariant[F] = generic + given product[F[_]: ProductInstancesOf[Contravariant]]: DerivedContravariant[F] = + new Product[Contravariant, F] {} + given coproduct[F[_]](using inst: => CoproductInstances[Contravariant |: Derived, F]): DerivedContravariant[F] = - generic(using inst.unify) + given CoproductInstances[Contravariant, F] = inst.unify + new Coproduct[Contravariant, F] {} diff --git a/core/src/main/scala-3/cats/derived/DerivedFunctor.scala b/core/src/main/scala-3/cats/derived/DerivedFunctor.scala index 7df5259c..fa2a28c0 100644 --- a/core/src/main/scala-3/cats/derived/DerivedFunctor.scala +++ b/core/src/main/scala-3/cats/derived/DerivedFunctor.scala @@ -1,6 +1,6 @@ package cats.derived -import cats.{Contravariant, Functor} +import cats.{Contravariant, Eval, Functor} import shapeless3.deriving.{Const, Derived} import shapeless3.deriving.K1.* @@ -41,8 +41,11 @@ object DerivedFunctor: ): DerivedFunctor[F <<< G] = F.unify.compose(using G.unify) - given [F[_]](using inst: => Instances[Functor |: Derived, F]): DerivedFunctor[F] = - generic(using inst.unify) + given [F[_]](using inst: ProductInstances[Functor |: Derived, F]): DerivedFunctor[F] = + Strict.product(using inst.unify) + + given [F[_]](using => CoproductInstances[Functor |: Derived, F]): DerivedFunctor[F] = + Strict.coproduct @deprecated("Kept for binary compatibility", "3.2.0") protected given [F[_], G[_]](using @@ -58,14 +61,39 @@ object DerivedFunctor: ): DerivedFunctor[[x] =>> F[G[x]]] = nested(using F, G) - private def generic[F[_]: InstancesOf[Functor]]: DerivedFunctor[F] = - new Generic[Functor, F] {} + private[derived] trait Safe[F[_]] extends Functor[F]: + private[derived] def safeMap[A, B](fa: F[A])(f: A => B): Eval[F[B]] + final override def map[A, B](fa: F[A])(f: A => B): F[B] = safeMap(fa)(f).value + + private[derived] def safeMap[F[_], A, B](F: Functor[F])(fa: F[A])(f: A => B): Eval[F[B]] = + F match + case safe: Safe[F] @scala.unchecked => safe.safeMap(fa)(f) + case _ => Eval.later(F.map(fa)(f)) trait Generic[T[f[_]] <: Functor[f], F[_]](using inst: Instances[T, F]) extends Functor[F]: final override def map[A, B](fa: F[A])(f: A => B): F[B] = inst.map(fa)([f[_]] => (F: T[f], fa: f[A]) => F.map(fa)(f)) + trait Product[T[f[_]] <: Functor[f], F[_]](using inst: ProductInstances[T, F]) extends Safe[F]: + private[derived] final override def safeMap[A, B](fa: F[A])(f: A => B): Eval[F[B]] = + val pure = [a] => (x: a) => Eval.now(x) + val mp = [a, b] => (ea: Eval[a], g: a => b) => ea.map(g) + val ap = [a, b] => (ef: Eval[a => b], ea: Eval[a]) => ef.flatMap(g => ea.map(g)) + inst.traverse[A, Eval, B](fa)(mp)(pure)(ap)( + [f[_]] => (F: T[f], fa: f[A]) => DerivedFunctor.safeMap(F)(fa)(f) + ) + + trait Coproduct[T[f[_]] <: Functor[f], F[_]](using inst: CoproductInstances[T, F]) extends Safe[F]: + private[derived] final override def safeMap[A, B](fa: F[A])(f: A => B): Eval[F[B]] = + Eval.defer(inst.fold(fa)( + [f[a] <: F[a]] => (F: T[f], fa: f[A]) => + DerivedFunctor.safeMap(F)(fa)(f).asInstanceOf[Eval[F[B]]] + )) + object Strict: - given product[F[_]: ProductInstancesOf[Functor]]: DerivedFunctor[F] = generic + given product[F[_]: ProductInstancesOf[Functor]]: DerivedFunctor[F] = + new Product[Functor, F] {} + given coproduct[F[_]](using inst: => CoproductInstances[Functor |: Derived, F]): DerivedFunctor[F] = - generic(using inst.unify) + given CoproductInstances[Functor, F] = inst.unify + new Coproduct[Functor, F] {} diff --git a/core/src/test/scala-3/cats/derived/ContravariantSuite.scala b/core/src/test/scala-3/cats/derived/ContravariantSuite.scala index 75cf7394..5fa5ccb6 100644 --- a/core/src/test/scala-3/cats/derived/ContravariantSuite.scala +++ b/core/src/test/scala-3/cats/derived/ContravariantSuite.scala @@ -56,6 +56,20 @@ class ContravariantSuite extends KittensSuite: validate("strict.semiauto.contravariant") testNoInstance("strict.semiauto.contravariant", "TopK") + locally: + import semiInstances.given + val Size = 10000 + test("semiauto.contravariant is stack safe for recursive ListPred"): + val longList: ListPred[Int] = List.fill(Size)((_: Int) => true) + val contramapped = Contravariant[ListPred].contramap(longList)((s: String) => s.length) + assertEquals(contramapped.size, Size) + + test("semiauto.contravariant is stack safe for recursive EnumK1Contra"): + val tree = (1 to Size).foldLeft[EnumK1Contra[Int]](EnumK1Contra.Leaf((_: Int) => ())): (acc, _) => + EnumK1Contra.Rec(EnumK1Contra.Leaf((_: Int) => ()), acc) + val contramapped = Contravariant[EnumK1Contra].contramap(tree)((s: String) => s.length) + assert(contramapped ne null) + locally: import derivedInstances.* val instance = "derived.contravariant" diff --git a/core/src/test/scala-3/cats/derived/FunctorSuite.scala b/core/src/test/scala-3/cats/derived/FunctorSuite.scala index cd4f7ead..286353eb 100644 --- a/core/src/test/scala-3/cats/derived/FunctorSuite.scala +++ b/core/src/test/scala-3/cats/derived/FunctorSuite.scala @@ -63,6 +63,20 @@ class FunctorSuite extends KittensSuite: validate("strict.semiauto.functor") testNoInstance("strict.semiauto.functor", "TopK") + locally: + import semiInstances.given + val Size = 50000 + test("semiauto.functor is stack safe for recursive IList"): + val list = (1 to Size).foldLeft[IList[Int]](INil())((acc, i) => ICons(i, acc)) + val mapped = Functor[IList].map(list)(_ + 1) + assertEquals(IList.toList(mapped).size, Size) + + test("semiauto.functor is stack safe for recursive EnumK1"): + val tree = (1 to Size).foldLeft[EnumK1[Int]](EnumK1.Leaf(0)): (acc, i) => + EnumK1.Rec(EnumK1.Leaf(i), acc) + val mapped = Functor[EnumK1].map(tree)(_ + 1) + assert(mapped ne null) + locally: import derivedInstances.* val instance = "derived.functor" From 1b4d7fc43a84ba91c1b98824fb5001fed686fd1c Mon Sep 17 00:00:00 2001 From: ismat Date: Fri, 8 May 2026 13:59:39 +0300 Subject: [PATCH 2/3] Add stack-safe recursion support for remaining typeclasses --- .../cats/derived/DerivedBifoldable.scala | 39 +++++- .../cats/derived/DerivedBifunctor.scala | 44 +++++-- .../cats/derived/DerivedBitraverse.scala | 63 ++++++---- .../cats/derived/DerivedContravariant.scala | 2 +- .../main/scala-3/cats/derived/DerivedEq.scala | 31 +++-- .../cats/derived/DerivedFoldable.scala | 26 +++- .../scala-3/cats/derived/DerivedFunctor.scala | 2 +- .../scala-3/cats/derived/DerivedHash.scala | 31 +++-- .../cats/derived/DerivedInvariant.scala | 42 +++++-- .../derived/DerivedNonEmptyTraverse.scala | 26 ++-- .../scala-3/cats/derived/DerivedOrder.scala | 36 ++++-- .../cats/derived/DerivedPartialOrder.scala | 36 ++++-- .../cats/derived/DerivedReducible.scala | 42 +++++-- .../scala-3/cats/derived/DerivedShow.scala | 47 ++++--- .../cats/derived/DerivedTraverse.scala | 44 +++++-- .../cats/derived/StackSafetySuite.scala | 119 ++++++++++++++++++ 16 files changed, 485 insertions(+), 145 deletions(-) create mode 100644 core/src/test/scala-3/cats/derived/StackSafetySuite.scala diff --git a/core/src/main/scala-3/cats/derived/DerivedBifoldable.scala b/core/src/main/scala-3/cats/derived/DerivedBifoldable.scala index 9330f2cd..d68917e1 100644 --- a/core/src/main/scala-3/cats/derived/DerivedBifoldable.scala +++ b/core/src/main/scala-3/cats/derived/DerivedBifoldable.scala @@ -74,9 +74,30 @@ object DerivedBifoldable: given [F[_, _]](using => CoproductInstances[Bifoldable |: Derived, F]): DerivedBifoldable[F] = Strict.coproduct - trait Product[T[f[_, _]] <: Bifoldable[f], F[_, _]](using inst: ProductInstances[T, F]) extends Bifoldable[F]: - final override def bifoldLeft[A, B, C](fab: F[A, B], c: C)(f: (C, A) => C, g: (C, B) => C): C = - inst.foldLeft(fab)(c)([f[_, _]] => (c: C, F: T[f], fab: f[A, B]) => F.bifoldLeft(fab, c)(f, g)) + private[derived] trait Safe[F[_, _]] extends Bifoldable[F]: + private[derived] def safeBifoldLeft[A, B, C](fab: F[A, B], c: Eval[C])( + f: (C, A) => C, + g: (C, B) => C + ): Eval[C] + override def bifoldLeft[A, B, C](fab: F[A, B], c: C)(f: (C, A) => C, g: (C, B) => C): C = + safeBifoldLeft(fab, Eval.now(c))(f, g).value + + private[derived] def safeBifoldLeft[F[_, _], A, B, C](F: Bifoldable[F])(fab: F[A, B], c: Eval[C])( + f: (C, A) => C, + g: (C, B) => C + ): Eval[C] = + F match + case safe: Safe[F] @scala.unchecked => safe.safeBifoldLeft(fab, c)(f, g) + case _ => c.map(F.bifoldLeft(fab, _)(f, g)) + + trait Product[T[f[_, _]] <: Bifoldable[f], F[_, _]](using inst: ProductInstances[T, F]) extends Safe[F]: + private[derived] final override def safeBifoldLeft[A, B, C](fab: F[A, B], c: Eval[C])( + f: (C, A) => C, + g: (C, B) => C + ): Eval[C] = + inst.foldLeft[A, B, Eval[C]](fab)(c): + [f[_, _]] => (acc: Eval[C], F: T[f], fab: f[A, B]) => + DerivedBifoldable.safeBifoldLeft(F)(fab, acc)(f, g) final override def bifoldRight[A, B, C](fab: F[A, B], c: Eval[C])( f: (A, Eval[C]) => Eval[C], @@ -84,9 +105,15 @@ object DerivedBifoldable: ): Eval[C] = inst.foldRight(fab)(c): [f[_, _]] => (F: T[f], fab: f[A, B], c: Eval[C]) => Eval.defer(F.bifoldRight(fab, c)(f, g)) - trait Coproduct[T[f[_, _]] <: Bifoldable[f], F[_, _]](using inst: CoproductInstances[T, F]) extends Bifoldable[F]: - final override def bifoldLeft[A, B, C](fab: F[A, B], c: C)(f: (C, A) => C, g: (C, B) => C): C = - inst.fold(fab)([f[_, _]] => (F: T[f], fab: f[A, B]) => F.bifoldLeft(fab, c)(f, g)) + trait Coproduct[T[f[_, _]] <: Bifoldable[f], F[_, _]](using inst: CoproductInstances[T, F]) extends Safe[F]: + private[derived] final override def safeBifoldLeft[A, B, C](fab: F[A, B], c: Eval[C])( + f: (C, A) => C, + g: (C, B) => C + ): Eval[C] = + Eval.defer(inst.fold(fab): + [f[_, _]] => (F: T[f], fab: f[A, B]) => + DerivedBifoldable.safeBifoldLeft(F)(fab, c)(f, g) + ) final override def bifoldRight[A, B, C](fab: F[A, B], c: Eval[C])( f: (A, Eval[C]) => Eval[C], diff --git a/core/src/main/scala-3/cats/derived/DerivedBifunctor.scala b/core/src/main/scala-3/cats/derived/DerivedBifunctor.scala index b75653c2..cb196251 100644 --- a/core/src/main/scala-3/cats/derived/DerivedBifunctor.scala +++ b/core/src/main/scala-3/cats/derived/DerivedBifunctor.scala @@ -1,6 +1,6 @@ package cats.derived -import cats.{Bifunctor, Functor} +import cats.{Bifunctor, Eval, Functor} import shapeless3.deriving.Derived import shapeless3.deriving.K2.* @@ -46,17 +46,47 @@ object DerivedBifunctor: new Lazy(() => F.unify.compose(using G.unify)) with Bifunctor[[a, b] =>> F[G[a, b], G[a, b]]]: export delegate.* - given generic[F[_, _]](using inst: => Instances[Bifunctor |: Derived, F]): DerivedBifunctor[F] = - gen(using inst.unify) + given [F[_, _]](using inst: ProductInstances[Bifunctor |: Derived, F]): DerivedBifunctor[F] = + Strict.product(using inst.unify) - private def gen[F[_, _]: InstancesOf[Bifunctor]]: DerivedBifunctor[F] = - new Generic[Bifunctor, F] {} + given [F[_, _]](using => CoproductInstances[Bifunctor |: Derived, F]): DerivedBifunctor[F] = + Strict.coproduct + + private[derived] trait Safe[F[_, _]] extends Bifunctor[F]: + private[derived] def safeBimap[A, B, C, D](fab: F[A, B])(f: A => C, g: B => D): Eval[F[C, D]] + override def bimap[A, B, C, D](fab: F[A, B])(f: A => C, g: B => D): F[C, D] = safeBimap(fab)(f, g).value + + private[derived] def safeBimap[F[_, _], A, B, C, D]( + F: Bifunctor[F] + )(fab: F[A, B])(f: A => C, g: B => D): Eval[F[C, D]] = + F match + case safe: Safe[F] @scala.unchecked => safe.safeBimap(fab)(f, g) + case _ => Eval.later(F.bimap(fab)(f, g)) trait Generic[T[f[_, _]] <: Bifunctor[f], F[_, _]](using inst: Instances[T, F]) extends Bifunctor[F]: final override def bimap[A, B, C, D](fab: F[A, B])(f: A => C, g: B => D): F[C, D] = inst.map(fab)([f[_, _]] => (F: T[f], fa: f[A, B]) => F.bimap(fa)(f, g)) + trait Product[T[f[_, _]] <: Bifunctor[f], F[_, _]](using inst: ProductInstances[T, F]) extends Safe[F]: + private[derived] final override def safeBimap[A, B, C, D](fab: F[A, B])(f: A => C, g: B => D): Eval[F[C, D]] = + val pure = [a] => (x: a) => Eval.now(x) + val mp = [a, b] => (ea: Eval[a], h: a => b) => ea.map(h) + val ap = [a, b] => (ef: Eval[a => b], ea: Eval[a]) => ef.flatMap(h => ea.map(h)) + inst.traverse[A, B, Eval, C, D](fab)(mp)(pure)(ap)( + [f[_, _]] => (F: T[f], fa: f[A, B]) => DerivedBifunctor.safeBimap(F)(fa)(f, g) + ) + + trait Coproduct[T[f[_, _]] <: Bifunctor[f], F[_, _]](using inst: CoproductInstances[T, F]) extends Safe[F]: + private[derived] final override def safeBimap[A, B, C, D](fab: F[A, B])(f: A => C, g: B => D): Eval[F[C, D]] = + Eval.defer(inst.fold(fab): + [f[a, b] <: F[a, b]] => (F: T[f], fa: f[A, B]) => + DerivedBifunctor.safeBimap(F)(fa)(f, g).asInstanceOf[Eval[F[C, D]]] + ) + object Strict: - given product[F[_, _]: ProductInstancesOf[Bifunctor]]: DerivedBifunctor[F] = gen + given product[F[_, _]: ProductInstancesOf[Bifunctor]]: DerivedBifunctor[F] = + new Product[Bifunctor, F] {} + given coproduct[F[_, _]](using inst: => CoproductInstances[Bifunctor |: Derived, F]): DerivedBifunctor[F] = - gen(using inst.unify) + given CoproductInstances[Bifunctor, F] = inst.unify + new Coproduct[Bifunctor, F] {} diff --git a/core/src/main/scala-3/cats/derived/DerivedBitraverse.scala b/core/src/main/scala-3/cats/derived/DerivedBitraverse.scala index bf108691..cc98164a 100644 --- a/core/src/main/scala-3/cats/derived/DerivedBitraverse.scala +++ b/core/src/main/scala-3/cats/derived/DerivedBitraverse.scala @@ -88,33 +88,54 @@ object DerivedBitraverse: given [F[_, _]](using => CoproductInstances[Bitraverse |: Derived, F]): DerivedBitraverse[F] = Strict.coproduct - trait Product[T[f[_, _]] <: Bitraverse[f], F[_, _]](using inst: ProductInstances[T, F]) - extends Bitraverse[F] - with DerivedBifunctor.Generic[T, F] - with DerivedBifoldable.Product[T, F]: - - final override def bitraverse[G[_], A, B, C, D](fab: F[A, B])(f: A => G[C], g: B => G[D])(using + private[derived] trait Safe[F[_, _]] extends Bitraverse[F]: + private[derived] def safeBitraverse[G[_], A, B, C, D](fab: F[A, B])(f: A => G[C], g: B => G[D])(using G: Applicative[G] - ): G[F[C, D]] = - val pure = [a] => (x: a) => G.pure(x) - val map = [a, b] => (ga: G[a], f: a => b) => G.map(ga)(f) - val ap = [a, b] => (gf: G[a => b], ga: G[a]) => G.ap(gf)(ga) - inst.traverse[A, B, G, C, D](fab)(map)(pure)(ap)([f[_, _]] => (F: T[f], fab: f[A, B]) => F.bitraverse(fab)(f, g)) + ): Eval[G[F[C, D]]] + override def bitraverse[G[_], A, B, C, D](fab: F[A, B])(f: A => G[C], g: B => G[D])(using + G: Applicative[G] + ): G[F[C, D]] = safeBitraverse(fab)(f, g).value - trait Coproduct[T[f[_, _]] <: Bitraverse[f], F[_, _]](using inst: CoproductInstances[T, F]) - extends Bitraverse[F] - with DerivedBifunctor.Generic[T, F] - with DerivedBifoldable.Coproduct[T, F]: + private[derived] def safeBitraverse[F[_, _], G[_], A, B, C, D]( + F: Bitraverse[F] + )(fab: F[A, B])(f: A => G[C], g: B => G[D])(using G: Applicative[G]): Eval[G[F[C, D]]] = + F match + case safe: Safe[F] @scala.unchecked => safe.safeBitraverse(fab)(f, g) + case _ => Eval.later(F.bitraverse(fab)(f, g)) - final override def bitraverse[G[_], A, B, C, D](fab: F[A, B])(f: A => G[C], g: B => G[D])(using - G: Applicative[G] - ): G[F[C, D]] = inst.fold(fab): - [f[a, b] <: F[a, b]] => (F: T[f], fa: f[A, B]) => G.widen[f[C, D], F[C, D]](F.bitraverse(fa)(f, g)) + trait Product[T[f[_, _]] <: Bitraverse[f], F[_, _]](using inst: ProductInstances[T, F]) + extends Safe[F], + DerivedBifunctor.Product[T, F], + DerivedBifoldable.Product[T, F]: + + private[derived] final override def safeBitraverse[G[_], A, B, C, D]( + fab: F[A, B] + )(f: A => G[C], g: B => G[D])(using G: Applicative[G]): Eval[G[F[C, D]]] = + val pure = [a] => (x: a) => Eval.now(G.pure(x)) + val mp = [a, b] => (ega: Eval[G[a]], h: a => b) => ega.map(ga => G.map(ga)(h)) + val ap = [a, b] => (egf: Eval[G[a => b]], ega: Eval[G[a]]) => + egf.flatMap(gf => ega.map(ga => G.ap(gf)(ga))) + inst.traverse[A, B, [x] =>> Eval[G[x]], C, D](fab)(mp)(pure)(ap)( + [f[_, _]] => (F: T[f], fab: f[A, B]) => DerivedBitraverse.safeBitraverse(F)(fab)(f, g) + ) + + trait Coproduct[T[f[_, _]] <: Bitraverse[f], F[_, _]](using inst: CoproductInstances[T, F]) + extends Safe[F], + DerivedBifunctor.Coproduct[T, F], + DerivedBifoldable.Coproduct[T, F]: + + private[derived] final override def safeBitraverse[G[_], A, B, C, D]( + fab: F[A, B] + )(f: A => G[C], g: B => G[D])(using G: Applicative[G]): Eval[G[F[C, D]]] = + Eval.defer(inst.fold(fab): + [f[a, b] <: F[a, b]] => (F: T[f], fa: f[A, B]) => + DerivedBitraverse.safeBitraverse(F)(fa)(f, g).map(g0 => G.widen[f[C, D], F[C, D]](g0)).asInstanceOf[Eval[G[F[C, D]]]] + ) object Strict: given product[F[_, _]: ProductInstancesOf[Bitraverse]]: DerivedBitraverse[F] = - new Bitraverse[F] with Product[Bitraverse, F] {} + new Product[Bitraverse, F] {} given coproduct[F[_, _]](using inst: => CoproductInstances[Bitraverse |: Derived, F]): DerivedBitraverse[F] = given CoproductInstances[Bitraverse, F] = inst.unify - new Bitraverse[F] with Coproduct[Bitraverse, F] {} + new Coproduct[Bitraverse, F] {} diff --git a/core/src/main/scala-3/cats/derived/DerivedContravariant.scala b/core/src/main/scala-3/cats/derived/DerivedContravariant.scala index 44b529d2..3c637792 100644 --- a/core/src/main/scala-3/cats/derived/DerivedContravariant.scala +++ b/core/src/main/scala-3/cats/derived/DerivedContravariant.scala @@ -46,7 +46,7 @@ object DerivedContravariant: private[derived] trait Safe[F[_]] extends Contravariant[F]: private[derived] def safeContramap[A, B](fa: F[A])(f: B => A): Eval[F[B]] - final override def contramap[A, B](fa: F[A])(f: B => A): F[B] = safeContramap(fa)(f).value + override def contramap[A, B](fa: F[A])(f: B => A): F[B] = safeContramap(fa)(f).value private[derived] def safeContramap[F[_], A, B](F: Contravariant[F])(fa: F[A])(f: B => A): Eval[F[B]] = F match diff --git a/core/src/main/scala-3/cats/derived/DerivedEq.scala b/core/src/main/scala-3/cats/derived/DerivedEq.scala index cbbf69a7..2f926366 100644 --- a/core/src/main/scala-3/cats/derived/DerivedEq.scala +++ b/core/src/main/scala-3/cats/derived/DerivedEq.scala @@ -1,6 +1,6 @@ package cats.derived -import cats.Eq +import cats.{Eq, Eval} import shapeless3.deriving.{Complete, Derived} import shapeless3.deriving.K0.* @@ -33,13 +33,28 @@ object DerivedEq: given CoproductInstances[Eq, A] = inst.unify new Coproduct[Eq, A] {} - trait Product[F[x] <: Eq[x], A](using inst: ProductInstances[F, A]) extends Eq[A]: - final override 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)(acc) - - trait Coproduct[F[x] <: Eq[x], A](using inst: CoproductInstances[F, A]) extends Eq[A]: - final override def eqv(x: A, y: A): Boolean = inst.fold2(x, y)(false): - [t] => (eqt: F[t], x: t, y: t) => eqt.eqv(x, y) + private[derived] trait Safe[A] extends Eq[A]: + private[derived] def safeEqv(x: A, y: A): Eval[Boolean] + override def eqv(x: A, y: A): Boolean = safeEqv(x, y).value + + private[derived] def safeEqv[A](F: Eq[A])(x: A, y: A): Eval[Boolean] = + F.asInstanceOf[Matchable] match + case safe: Safe[?] => safe.asInstanceOf[Safe[A]].safeEqv(x, y) + case _ => Eval.later(F.eqv(x, y)) + + trait Product[F[x] <: Eq[x], A](using inst: ProductInstances[F, A]) extends Safe[A]: + private[derived] final override def safeEqv(x: A, y: A): Eval[Boolean] = + inst.foldLeft2[Eval[Boolean]](x, y)(Eval.now(true)): + [t] => (acc: Eval[Boolean], eqt: F[t], xt: t, yt: t) => + val next = acc.flatMap: b => + if !b then Eval.now(false) else DerivedEq.safeEqv(eqt)(xt, yt) + Complete(false)(next)(next) + + trait Coproduct[F[x] <: Eq[x], A](using inst: CoproductInstances[F, A]) extends Safe[A]: + private[derived] final override def safeEqv(x: A, y: A): Eval[Boolean] = + Eval.defer(inst.fold2(x, y)(Eval.now(false): Eval[Boolean]): + [t] => (eqt: F[t], xt: t, yt: t) => DerivedEq.safeEqv(eqt)(xt, yt) + ) object Strict: export DerivedEq.coproduct diff --git a/core/src/main/scala-3/cats/derived/DerivedFoldable.scala b/core/src/main/scala-3/cats/derived/DerivedFoldable.scala index 95f97775..785387f8 100644 --- a/core/src/main/scala-3/cats/derived/DerivedFoldable.scala +++ b/core/src/main/scala-3/cats/derived/DerivedFoldable.scala @@ -44,16 +44,30 @@ object DerivedFoldable: @deprecated("Kept for binary compatibility", "3.2.0") protected given [F[_]: Foldable |: Derived, G[_]: Foldable |: Derived]: DerivedFoldable[[x] =>> F[G[x]]] = nested - trait Product[T[f[_]] <: Foldable[f], F[_]](using inst: ProductInstances[T, F]) extends Foldable[F]: - final override def foldLeft[A, B](fa: F[A], b: B)(f: (B, A) => B): B = - inst.foldLeft(fa)(b)([f[_]] => (b: B, F: T[f], fa: f[A]) => F.foldLeft(fa, b)(f)) + private[derived] trait Safe[F[_]] extends Foldable[F]: + private[derived] def safeFoldLeft[A, B](fa: F[A], b: Eval[B])(f: (B, A) => B): Eval[B] + override def foldLeft[A, B](fa: F[A], b: B)(f: (B, A) => B): B = + safeFoldLeft(fa, Eval.now(b))(f).value + + private[derived] def safeFoldLeft[F[_], A, B](F: Foldable[F])(fa: F[A], b: Eval[B])(f: (B, A) => B): Eval[B] = + F match + case safe: Safe[F] @scala.unchecked => safe.safeFoldLeft(fa, b)(f) + case _ => b.map(F.foldLeft(fa, _)(f)) + + trait Product[T[f[_]] <: Foldable[f], F[_]](using inst: ProductInstances[T, F]) extends Safe[F]: + private[derived] final override def safeFoldLeft[A, B](fa: F[A], b: Eval[B])(f: (B, A) => B): Eval[B] = + inst.foldLeft[A, Eval[B]](fa)(b): + [f[_]] => (acc: Eval[B], F: T[f], fa: f[A]) => + DerivedFoldable.safeFoldLeft(F)(fa, acc)(f) final override def foldRight[A, B](fa: F[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = inst.foldRight(fa)(lb)([f[_]] => (F: T[f], fa: f[A], lb: Eval[B]) => Eval.defer(F.foldRight(fa, lb)(f))) - trait Coproduct[T[f[_]] <: Foldable[f], F[_]](using inst: CoproductInstances[T, F]) extends Foldable[F]: - final override def foldLeft[A, B](fa: F[A], b: B)(f: (B, A) => B): B = - inst.fold(fa)([f[_]] => (F: T[f], fa: f[A]) => F.foldLeft(fa, b)(f)) + trait Coproduct[T[f[_]] <: Foldable[f], F[_]](using inst: CoproductInstances[T, F]) extends Safe[F]: + private[derived] final override def safeFoldLeft[A, B](fa: F[A], b: Eval[B])(f: (B, A) => B): Eval[B] = + Eval.defer(inst.fold(fa)([f[_]] => (F: T[f], fa: f[A]) => + DerivedFoldable.safeFoldLeft(F)(fa, b)(f) + )) final override def foldRight[A, B](fa: F[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = inst.fold(fa)([f[_]] => (F: T[f], fa: f[A]) => Eval.defer(F.foldRight(fa, lb)(f))) diff --git a/core/src/main/scala-3/cats/derived/DerivedFunctor.scala b/core/src/main/scala-3/cats/derived/DerivedFunctor.scala index fa2a28c0..53673abb 100644 --- a/core/src/main/scala-3/cats/derived/DerivedFunctor.scala +++ b/core/src/main/scala-3/cats/derived/DerivedFunctor.scala @@ -63,7 +63,7 @@ object DerivedFunctor: private[derived] trait Safe[F[_]] extends Functor[F]: private[derived] def safeMap[A, B](fa: F[A])(f: A => B): Eval[F[B]] - final override def map[A, B](fa: F[A])(f: A => B): F[B] = safeMap(fa)(f).value + override def map[A, B](fa: F[A])(f: A => B): F[B] = safeMap(fa)(f).value private[derived] def safeMap[F[_], A, B](F: Functor[F])(fa: F[A])(f: A => B): Eval[F[B]] = F match diff --git a/core/src/main/scala-3/cats/derived/DerivedHash.scala b/core/src/main/scala-3/cats/derived/DerivedHash.scala index 32b92635..b83248b3 100644 --- a/core/src/main/scala-3/cats/derived/DerivedHash.scala +++ b/core/src/main/scala-3/cats/derived/DerivedHash.scala @@ -1,6 +1,6 @@ package cats.derived -import cats.Hash +import cats.{Eval, Hash} import shapeless3.deriving.Derived import shapeless3.deriving.K0.* @@ -23,7 +23,6 @@ object DerivedHash: import Strict.given summonInline[DerivedHash[A]].instance - // These instances support singleton types unlike the instances in Cats' kernel. given boolean[A <: Boolean]: DerivedHash[A] = Hash.fromUniversalHashCode given byte[A <: Byte]: DerivedHash[A] = Hash.fromUniversalHashCode given short[A <: Short]: DerivedHash[A] = Hash.fromUniversalHashCode @@ -42,21 +41,33 @@ object DerivedHash: given CoproductInstances[Hash, A] = inst.unify new Coproduct[Hash, A] {} + private[derived] trait Safe[A] extends Hash[A]: + private[derived] def safeHash(x: A): Eval[Int] + override def hash(x: A): Int = safeHash(x).value + + private[derived] def safeHash[A](F: Hash[A])(x: A): Eval[Int] = + F.asInstanceOf[Matchable] match + case safe: Safe[?] => safe.asInstanceOf[Safe[A]].safeHash(x) + case _ => Eval.later(F.hash(x)) + trait Product[F[x] <: Hash[x], A <: scala.Product](using inst: ProductInstances[F, A]) extends DerivedEq.Product[F, A], - Hash[A]: + Safe[A]: - final override def hash(x: A): Int = + private[derived] final override def safeHash(x: A): Eval[Int] = val arity = x.productArity val prefix = x.productPrefix.hashCode - if arity <= 0 then prefix + if arity <= 0 then Eval.now(prefix) else - val hash = inst.foldLeft[Int](x)(MurmurHash3.mix(MurmurHash3.productSeed, prefix)): - [t] => (acc: Int, h: F[t], x: t) => MurmurHash3.mix(acc, h.hash(x)) - MurmurHash3.finalizeHash(hash, arity) + val seed = MurmurHash3.mix(MurmurHash3.productSeed, prefix) + inst.foldLeft[Eval[Int]](x)(Eval.now(seed)): + [t] => (acc: Eval[Int], h: F[t], xt: t) => + acc.flatMap(a => DerivedHash.safeHash(h)(xt).map(hh => MurmurHash3.mix(a, hh))) + .map(MurmurHash3.finalizeHash(_, arity)) - trait Coproduct[F[x] <: Hash[x], A](using inst: CoproductInstances[F, A]) extends DerivedEq.Coproduct[F, A], Hash[A]: - final override def hash(x: A): Int = inst.fold[Int](x)([t] => (h: F[t], x: t) => h.hash(x)) + trait Coproduct[F[x] <: Hash[x], A](using inst: CoproductInstances[F, A]) extends DerivedEq.Coproduct[F, A], Safe[A]: + private[derived] final override def safeHash(x: A): Eval[Int] = + Eval.defer(inst.fold[Eval[Int]](x)([t] => (h: F[t], xt: t) => DerivedHash.safeHash(h)(xt))) object Strict: export DerivedHash.coproduct diff --git a/core/src/main/scala-3/cats/derived/DerivedInvariant.scala b/core/src/main/scala-3/cats/derived/DerivedInvariant.scala index 46cd3407..fe326afc 100644 --- a/core/src/main/scala-3/cats/derived/DerivedInvariant.scala +++ b/core/src/main/scala-3/cats/derived/DerivedInvariant.scala @@ -1,6 +1,6 @@ package cats.derived -import cats.Invariant +import cats.{Eval, Invariant} import shapeless3.deriving.{Const, Derived} import shapeless3.deriving.K1.* @@ -34,21 +34,49 @@ object DerivedInvariant: new Lazy(() => F.unify.compose(using G.unify)) with Invariant[F <<< G]: export delegate.* - given [F[_]](using inst: => Instances[Invariant |: Derived, F]): DerivedInvariant[F] = - generic(using inst.unify) + given [F[_]](using inst: ProductInstances[Invariant |: Derived, F]): DerivedInvariant[F] = + Strict.product(using inst.unify) + + given [F[_]](using => CoproductInstances[Invariant |: Derived, F]): DerivedInvariant[F] = + Strict.coproduct @deprecated("Kept for binary compatibility", "3.2.0") protected given [F[_]: Invariant |: Derived, G[_]: Invariant |: Derived]: DerivedInvariant[[x] =>> F[G[x]]] = nested - private def generic[F[_]: InstancesOf[Invariant]]: DerivedInvariant[F] = - new Generic[Invariant, F] {} + private[derived] trait Safe[F[_]] extends Invariant[F]: + private[derived] def safeImap[A, B](fa: F[A])(f: A => B)(g: B => A): Eval[F[B]] + override def imap[A, B](fa: F[A])(f: A => B)(g: B => A): F[B] = safeImap(fa)(f)(g).value + + private[derived] def safeImap[F[_], A, B](F: Invariant[F])(fa: F[A])(f: A => B)(g: B => A): Eval[F[B]] = + F match + case safe: Safe[F] @scala.unchecked => safe.safeImap(fa)(f)(g) + case _ => Eval.later(F.imap(fa)(f)(g)) trait Generic[T[f[_]] <: Invariant[f], F[_]](using inst: Instances[T, F]) extends Invariant[F]: final override def imap[A, B](fa: F[A])(f: A => B)(g: B => A): F[B] = inst.map(fa)([f[_]] => (F: T[f], fa: f[A]) => F.imap(fa)(f)(g)) + trait Product[T[f[_]] <: Invariant[f], F[_]](using inst: ProductInstances[T, F]) extends Safe[F]: + private[derived] final override def safeImap[A, B](fa: F[A])(f: A => B)(g: B => A): Eval[F[B]] = + val pure = [a] => (x: a) => Eval.now(x) + val mp = [a, b] => (ea: Eval[a], h: a => b) => ea.map(h) + val ap = [a, b] => (ef: Eval[a => b], ea: Eval[a]) => ef.flatMap(h => ea.map(h)) + inst.traverse[A, Eval, B](fa)(mp)(pure)(ap)( + [f[_]] => (F: T[f], fa: f[A]) => DerivedInvariant.safeImap(F)(fa)(f)(g) + ) + + trait Coproduct[T[f[_]] <: Invariant[f], F[_]](using inst: CoproductInstances[T, F]) extends Safe[F]: + private[derived] final override def safeImap[A, B](fa: F[A])(f: A => B)(g: B => A): Eval[F[B]] = + Eval.defer(inst.fold(fa)( + [f[a] <: F[a]] => (F: T[f], fa: f[A]) => + DerivedInvariant.safeImap(F)(fa)(f)(g).asInstanceOf[Eval[F[B]]] + )) + object Strict: - given product[F[_]: ProductInstancesOf[Invariant]]: DerivedInvariant[F] = generic + given product[F[_]: ProductInstancesOf[Invariant]]: DerivedInvariant[F] = + new Product[Invariant, F] {} + given coproduct[F[_]](using inst: => CoproductInstances[Invariant |: Derived, F]): DerivedInvariant[F] = - generic(using inst.unify) + given CoproductInstances[Invariant, F] = inst.unify + new Coproduct[Invariant, F] {} diff --git a/core/src/main/scala-3/cats/derived/DerivedNonEmptyTraverse.scala b/core/src/main/scala-3/cats/derived/DerivedNonEmptyTraverse.scala index d071acc0..4e6c9176 100644 --- a/core/src/main/scala-3/cats/derived/DerivedNonEmptyTraverse.scala +++ b/core/src/main/scala-3/cats/derived/DerivedNonEmptyTraverse.scala @@ -1,6 +1,6 @@ package cats.derived -import cats.{Applicative, Apply, NonEmptyTraverse, Traverse} +import cats.{Applicative, Apply, Eval, NonEmptyTraverse, Traverse} import shapeless3.deriving.Derived import shapeless3.deriving.K1.* @@ -54,7 +54,8 @@ object DerivedNonEmptyTraverse: DerivedTraverse.Product[T, F]: final override def nonEmptyTraverse[G[_]: Apply, A, B](fa: F[A])(f: A => G[B]): G[F[B]] = - traverse[Alt[G], A, B](fa)(f.andThen(Left.apply)) match + given Applicative[Alt[G]] = altApplicative[G] + DerivedTraverse.safeTraverse[F, Alt[G], A, B](this)(fa)(f.andThen(Left.apply)).value match case Left(value) => value case Right(_) => ??? @@ -64,10 +65,18 @@ object DerivedNonEmptyTraverse: DerivedTraverse.Coproduct[T, F]: final override def nonEmptyTraverse[G[_], A, B](fa: F[A])(f: A => G[B])(using G: Apply[G]): G[F[B]] = - inst.fold(fa)([f[a] <: F[a]] => (F: T[f], fa: f[A]) => G.widen[f[B], F[B]](F.nonEmptyTraverse(fa)(f))) + safeNonEmptyTraverse(fa)(f).value + + private[derived] def safeNonEmptyTraverse[G[_], A, B]( + fa: F[A] + )(f: A => G[B])(using G: Apply[G]): Eval[G[F[B]]] = + Eval.defer(inst.fold(fa): + [f[a] <: F[a]] => (F: T[f], fa: f[A]) => + Eval.later(F.nonEmptyTraverse(fa)(f)).map(g => G.widen[f[B], F[B]](g)).asInstanceOf[Eval[G[F[B]]]] + ) private type Alt[F[_]] = [A] =>> Either[F[A], A] - private given [F[_]](using F: Apply[F]): Applicative[Alt[F]] with + private def altApplicative[F[_]](using F: Apply[F]): Applicative[Alt[F]] = new Applicative[Alt[F]]: override def pure[A](x: A) = Right(x) override def map[A, B](fa: Alt[F][A])(f: A => B) = fa match case Left(fa) => Left(F.map(fa)(f)) @@ -78,12 +87,11 @@ object DerivedNonEmptyTraverse: case (Right(f), Left(fa)) => Left(F.map(fa)(f)) case (Right(f), Right(a)) => Right(f(a)) + private given [F[_]](using F: Apply[F]): Applicative[Alt[F]] = altApplicative[F] + object Strict: def product[F[_]: ProductInstancesOf[Traverse]](ev: NonEmptyTraverse[?]): DerivedNonEmptyTraverse[F] = - new Product[Traverse, F](ev) - with DerivedReducible.Product[Traverse, F](ev) - with DerivedTraverse.Product[Traverse, F] - with DerivedFunctor.Generic[Traverse, F] {} + new Product[Traverse, F](ev) with DerivedReducible.Product[Traverse, F](ev) {} inline given product[F[_]](using gen: ProductGeneric[F]): DerivedNonEmptyTraverse[F] = product(summonFirst[NonEmptyTraverse, gen.MirroredElemTypes]) @@ -92,4 +100,4 @@ object DerivedNonEmptyTraverse: inst: => CoproductInstances[NonEmptyTraverse |: Derived, F] ): DerivedNonEmptyTraverse[F] = given CoproductInstances[NonEmptyTraverse, F] = inst.unify - new NonEmptyTraverse[F] with Coproduct[NonEmptyTraverse, F] {} + new Coproduct[NonEmptyTraverse, F] {} diff --git a/core/src/main/scala-3/cats/derived/DerivedOrder.scala b/core/src/main/scala-3/cats/derived/DerivedOrder.scala index 57b2056e..2569410c 100644 --- a/core/src/main/scala-3/cats/derived/DerivedOrder.scala +++ b/core/src/main/scala-3/cats/derived/DerivedOrder.scala @@ -1,6 +1,6 @@ package cats.derived -import cats.Order +import cats.{Eval, Order} import shapeless3.deriving.{Complete, Derived} import shapeless3.deriving.K0.* @@ -33,18 +33,28 @@ object DerivedOrder: given CoproductInstances[Order, A] = inst.unify new Coproduct[Order, A] {} - trait Product[T[x] <: Order[x], A](using inst: ProductInstances[T, A]) extends Order[A]: - def compare(x: A, y: A): Int = - inst.foldLeft2(x, y)(0: Int): - [t] => - (acc: Int, ord: T[t], t0: t, t1: t) => - val cmp = ord.compare(t0, t1) - Complete(cmp != 0)(cmp)(acc) - - trait Coproduct[T[x] <: Order[x], A](using inst: CoproductInstances[T, A]) extends Order[A]: - def compare(x: A, y: A): Int = - inst.fold2(x, y)((x: Int, y: Int) => x - y): - [t] => (ord: T[t], t0: t, t1: t) => ord.compare(t0, t1) + private[derived] trait Safe[A] extends Order[A]: + private[derived] def safeCompare(x: A, y: A): Eval[Int] + override def compare(x: A, y: A): Int = safeCompare(x, y).value + + private[derived] def safeCompare[A](F: Order[A])(x: A, y: A): Eval[Int] = + F.asInstanceOf[Matchable] match + case safe: Safe[?] => safe.asInstanceOf[Safe[A]].safeCompare(x, y) + case _ => Eval.later(F.compare(x, y)) + + trait Product[T[x] <: Order[x], A](using inst: ProductInstances[T, A]) extends Safe[A]: + private[derived] final override def safeCompare(x: A, y: A): Eval[Int] = + inst.foldLeft2[Eval[Int]](x, y)(Eval.now(0)): + [t] => (acc: Eval[Int], ord: T[t], t0: t, t1: t) => + val next = acc.flatMap: cmp => + if cmp != 0 then Eval.now(cmp) else DerivedOrder.safeCompare(ord)(t0, t1) + Complete(false)(next)(next) + + trait Coproduct[T[x] <: Order[x], A](using inst: CoproductInstances[T, A]) extends Safe[A]: + private[derived] final override def safeCompare(x: A, y: A): Eval[Int] = + Eval.defer(inst.fold2(x, y)((x: Int, y: Int) => Eval.now(x - y)): + [t] => (ord: T[t], t0: t, t1: t) => DerivedOrder.safeCompare(ord)(t0, t1) + ) object Strict: export DerivedOrder.coproduct diff --git a/core/src/main/scala-3/cats/derived/DerivedPartialOrder.scala b/core/src/main/scala-3/cats/derived/DerivedPartialOrder.scala index 50903601..dd476ca2 100644 --- a/core/src/main/scala-3/cats/derived/DerivedPartialOrder.scala +++ b/core/src/main/scala-3/cats/derived/DerivedPartialOrder.scala @@ -1,6 +1,6 @@ package cats.derived -import cats.{Order, PartialOrder} +import cats.{Eval, Order, PartialOrder} import shapeless3.deriving.{Complete, Derived} import shapeless3.deriving.K0.* @@ -33,18 +33,28 @@ object DerivedPartialOrder: given CoproductInstances[PartialOrder, A] = inst.unify new Coproduct[PartialOrder, A] {} - trait Product[T[x] <: PartialOrder[x], A](using inst: ProductInstances[T, A]) extends PartialOrder[A]: - def partialCompare(x: A, y: A): Double = - inst.foldLeft2(x, y)(0: Double): - [t] => - (acc: Double, ord: T[t], t0: t, t1: t) => - val cmp = ord.partialCompare(t0, t1) - Complete(cmp != 0)(cmp)(acc) - - trait Coproduct[T[x] <: PartialOrder[x], A](using inst: CoproductInstances[T, A]) extends PartialOrder[A]: - def partialCompare(x: A, y: A): Double = - inst.fold2(x, y)(Double.NaN: Double): - [t] => (ord: T[t], t0: t, t1: t) => ord.partialCompare(t0, t1) + private[derived] trait Safe[A] extends PartialOrder[A]: + private[derived] def safePartialCompare(x: A, y: A): Eval[Double] + override def partialCompare(x: A, y: A): Double = safePartialCompare(x, y).value + + private[derived] def safePartialCompare[A](F: PartialOrder[A])(x: A, y: A): Eval[Double] = + F.asInstanceOf[Matchable] match + case safe: Safe[?] => safe.asInstanceOf[Safe[A]].safePartialCompare(x, y) + case _ => Eval.later(F.partialCompare(x, y)) + + trait Product[T[x] <: PartialOrder[x], A](using inst: ProductInstances[T, A]) extends Safe[A]: + private[derived] final override def safePartialCompare(x: A, y: A): Eval[Double] = + inst.foldLeft2[Eval[Double]](x, y)(Eval.now(0.0)): + [t] => (acc: Eval[Double], ord: T[t], t0: t, t1: t) => + val next = acc.flatMap: cmp => + if cmp != 0.0 then Eval.now(cmp) else DerivedPartialOrder.safePartialCompare(ord)(t0, t1) + Complete(false)(next)(next) + + trait Coproduct[T[x] <: PartialOrder[x], A](using inst: CoproductInstances[T, A]) extends Safe[A]: + private[derived] final override def safePartialCompare(x: A, y: A): Eval[Double] = + Eval.defer(inst.fold2(x, y)(Eval.now(Double.NaN): Eval[Double]): + [t] => (ord: T[t], t0: t, t1: t) => DerivedPartialOrder.safePartialCompare(ord)(t0, t1) + ) object Strict: export DerivedPartialOrder.coproduct diff --git a/core/src/main/scala-3/cats/derived/DerivedReducible.scala b/core/src/main/scala-3/cats/derived/DerivedReducible.scala index bfd7aa82..f51d11ac 100644 --- a/core/src/main/scala-3/cats/derived/DerivedReducible.scala +++ b/core/src/main/scala-3/cats/derived/DerivedReducible.scala @@ -45,21 +45,35 @@ object DerivedReducible: protected given [F[_]: Reducible |: Derived, G[_]: Reducible |: Derived]: DerivedReducible[[x] =>> F[G[x]]] = nested + private[derived] trait Safe[F[_]] extends Reducible[F]: + private[derived] def safeReduceLeftTo[A, B](fa: F[A])(f: A => B)(g: (B, A) => B): Eval[B] + override def reduceLeftTo[A, B](fa: F[A])(f: A => B)(g: (B, A) => B): B = + safeReduceLeftTo(fa)(f)(g).value + + private[derived] def safeReduceLeftTo[F[_], A, B]( + F: Reducible[F] + )(fa: F[A])(f: A => B)(g: (B, A) => B): Eval[B] = + F match + case safe: Safe[F] @scala.unchecked => safe.safeReduceLeftTo(fa)(f)(g) + case _ => Eval.later(F.reduceLeftTo(fa)(f)(g)) + trait Product[T[f[_]] <: Foldable[f], F[_]](@unused ev: Reducible[?])(using inst: ProductInstances[T, F]) - extends DerivedFoldable.Product[T, F], - Reducible[F]: + extends Safe[F], + DerivedFoldable.Product[T, F]: private val evalNone = Eval.now(None) - final override def reduceLeftTo[A, B](fa: F[A])(f: A => B)(g: (B, A) => B): B = + private[derived] final override def safeReduceLeftTo[A, B](fa: F[A])(f: A => B)(g: (B, A) => B): Eval[B] = inst - .foldLeft[A, Option[B]](fa)(None): + .foldLeft[A, Eval[Option[B]]](fa)(evalNone): [f[_]] => - (acc: Option[B], F: T[f], fa: f[A]) => - acc match - case Some(b) => Some(F.foldLeft(fa, b)(g)) - case None => F.reduceLeftToOption(fa)(f)(g) - .get + (acc: Eval[Option[B]], F: T[f], fa: f[A]) => + acc.flatMap: + case Some(b) => DerivedFoldable.safeFoldLeft(F)(fa, Eval.now(b))(g).map(Some.apply) + case None => F match + case red: Reducible[F] @scala.unchecked => DerivedReducible.safeReduceLeftTo(red)(fa)(f)(g).map(Some.apply) + case _ => Eval.now(F.reduceLeftToOption(fa)(f)(g)) + .map(_.get) final override def reduceRightTo[A, B](fa: F[A])(f: A => B)(g: (A, Eval[B]) => Eval[B]): Eval[B] = inst @@ -72,11 +86,13 @@ object DerivedReducible: .map(_.get) trait Coproduct[T[f[_]] <: Reducible[f], F[_]](using inst: CoproductInstances[T, F]) - extends DerivedFoldable.Coproduct[T, F], - Reducible[F]: + extends Safe[F], + DerivedFoldable.Coproduct[T, F]: - final override def reduceLeftTo[A, B](fa: F[A])(f: A => B)(g: (B, A) => B): B = - inst.fold(fa)([f[_]] => (F: T[f], fa: f[A]) => F.reduceLeftTo(fa)(f)(g)) + private[derived] final override def safeReduceLeftTo[A, B](fa: F[A])(f: A => B)(g: (B, A) => B): Eval[B] = + Eval.defer(inst.fold(fa)([f[_]] => (F: T[f], fa: f[A]) => + DerivedReducible.safeReduceLeftTo(F)(fa)(f)(g) + )) final override def reduceRightTo[A, B](fa: F[A])(f: A => B)(g: (A, Eval[B]) => Eval[B]): Eval[B] = inst.fold(fa)([f[_]] => (F: T[f], fa: f[A]) => Eval.defer(F.reduceRightTo(fa)(f)(g))) diff --git a/core/src/main/scala-3/cats/derived/DerivedShow.scala b/core/src/main/scala-3/cats/derived/DerivedShow.scala index 6d4dfdc2..66e5b6b1 100644 --- a/core/src/main/scala-3/cats/derived/DerivedShow.scala +++ b/core/src/main/scala-3/cats/derived/DerivedShow.scala @@ -1,6 +1,6 @@ package cats.derived -import cats.Show +import cats.{Eval, Show} import shapeless3.deriving.{Derived, Labelling} import shapeless3.deriving.K0.* @@ -22,7 +22,6 @@ object DerivedShow: import Strict.given summonInline[DerivedShow[A]].instance - // These instances support singleton types unlike the instances in Cats' core. given boolean[A <: Boolean]: DerivedShow[A] = Show.fromToString given byte[A <: Byte]: DerivedShow[A] = Show.fromToString given short[A <: Short]: DerivedShow[A] = Show.fromToString @@ -41,30 +40,40 @@ object DerivedShow: given [A](using => CoproductInstances[Show |: Derived, A]): DerivedShow[A] = Strict.coproduct - trait Product[F[x] <: Show[x], A](using inst: ProductInstances[F, A], labelling: Labelling[A]) extends Show[A]: - def show(a: A): String = + private[derived] trait Safe[A] extends Show[A]: + private[derived] def safeShow(a: A): Eval[String] + override def show(a: A): String = safeShow(a).value + + private[derived] def safeShow[A](F: Show[A])(a: A): Eval[String] = + F match + case safe: Safe[?] => safe.asInstanceOf[Safe[A]].safeShow(a) + case _ => Eval.later(F.show(a)) + + trait Product[F[x] <: Show[x], A](using inst: ProductInstances[F, A], labelling: Labelling[A]) extends Safe[A]: + private[derived] final override def safeShow(a: A): Eval[String] = val prefix = labelling.label val labels = labelling.elemLabels val n = labels.size - if n <= 0 then prefix + if n <= 0 then Eval.now(prefix) else val sb = new StringBuilder(prefix) sb.append('(') - var i = 0 - while i < n do - sb.append(labels(i)) - sb.append(" = ") - sb.append(inst.project(a)(i)([t] => (show: F[t], x: t) => show.show(x))) - sb.append(", ") - i += 1 - - val l = sb.length - sb.delete(l - 2, l) - sb.append(')') - sb.toString + def loop(i: Int): Eval[StringBuilder] = + if i >= n then Eval.now(sb) + else + inst.project(a)(i)([t] => (showt: F[t], xt: t) => DerivedShow.safeShow(showt)(xt)).flatMap: rendered => + sb.append(labels(i)) + sb.append(" = ") + sb.append(rendered) + if i < n - 1 then sb.append(", ") + Eval.defer(loop(i + 1)) + loop(0).map: built => + built.append(')') + built.toString - trait Coproduct[F[x] <: Show[x], A](using inst: CoproductInstances[F, A]) extends Show[A]: - def show(a: A): String = inst.fold(a)([t] => (st: F[t], t: t) => st.show(t)) + trait Coproduct[F[x] <: Show[x], A](using inst: CoproductInstances[F, A]) extends Safe[A]: + private[derived] final override def safeShow(a: A): Eval[String] = + Eval.defer(inst.fold[Eval[String]](a)([t] => (st: F[t], t: t) => DerivedShow.safeShow(st)(t))) object Strict: given product[A: Labelling](using => ProductInstances[Show, A]): DerivedShow[A] = diff --git a/core/src/main/scala-3/cats/derived/DerivedTraverse.scala b/core/src/main/scala-3/cats/derived/DerivedTraverse.scala index fcbb7eb2..35db56ef 100644 --- a/core/src/main/scala-3/cats/derived/DerivedTraverse.scala +++ b/core/src/main/scala-3/cats/derived/DerivedTraverse.scala @@ -46,24 +46,46 @@ object DerivedTraverse: @deprecated("Kept for binary compatibility", "3.2.0") protected given [F[_]: Traverse |: Derived, G[_]: Traverse |: Derived]: DerivedTraverse[[x] =>> F[G[x]]] = nested + private[derived] trait Safe[F[_]] extends Traverse[F]: + private[derived] def safeTraverse[G[_], A, B](fa: F[A])(f: A => G[B])(using G: Applicative[G]): Eval[G[F[B]]] + override def traverse[G[_], A, B](fa: F[A])(f: A => G[B])(using G: Applicative[G]): G[F[B]] = + safeTraverse(fa)(f).value + + private[derived] def safeTraverse[F[_], G[_], A, B]( + F: Traverse[F] + )(fa: F[A])(f: A => G[B])(using G: Applicative[G]): Eval[G[F[B]]] = + F match + case safe: Safe[F] @scala.unchecked => safe.safeTraverse(fa)(f) + case _ => Eval.later(F.traverse(fa)(f)) + trait Product[T[f[_]] <: Traverse[f], F[_]](using inst: ProductInstances[T, F]) - extends Traverse[F], - DerivedFunctor.Generic[T, F], + extends Safe[F], + DerivedFunctor.Product[T, F], DerivedFoldable.Product[T, F]: - final override def traverse[G[_], A, B](fa: F[A])(f: A => G[B])(using G: Applicative[G]): G[F[B]] = - val pure = [a] => (x: a) => G.pure(x) - val map = [a, b] => (ga: G[a], f: a => b) => G.map(ga)(f) - val ap = [a, b] => (gf: G[a => b], ga: G[a]) => G.ap(gf)(ga) - inst.traverse[A, G, B](fa)(map)(pure)(ap)([f[_]] => (F: T[f], fa: f[A]) => F.traverse(fa)(f)) + private[derived] final override def safeTraverse[G[_], A, B]( + fa: F[A] + )(f: A => G[B])(using G: Applicative[G]): Eval[G[F[B]]] = + val pure = [a] => (x: a) => Eval.now(G.pure(x)) + val mp = [a, b] => (ega: Eval[G[a]], h: a => b) => ega.map(ga => G.map(ga)(h)) + val ap = [a, b] => (egf: Eval[G[a => b]], ega: Eval[G[a]]) => + egf.flatMap(gf => ega.map(ga => G.ap(gf)(ga))) + inst.traverse[A, [x] =>> Eval[G[x]], B](fa)(mp)(pure)(ap)( + [f[_]] => (F: T[f], fa: f[A]) => DerivedTraverse.safeTraverse(F)(fa)(f) + ) trait Coproduct[T[f[_]] <: Traverse[f], F[_]](using inst: CoproductInstances[T, F]) - extends Traverse[F], - DerivedFunctor.Generic[T, F], + extends Safe[F], + DerivedFunctor.Coproduct[T, F], DerivedFoldable.Coproduct[T, F]: - final override def traverse[G[_], A, B](fa: F[A])(f: A => G[B])(using G: Applicative[G]): G[F[B]] = - inst.fold(fa)([f[a] <: F[a]] => (F: T[f], fa: f[A]) => G.widen[f[B], F[B]](F.traverse(fa)(f))) + private[derived] final override def safeTraverse[G[_], A, B]( + fa: F[A] + )(f: A => G[B])(using G: Applicative[G]): Eval[G[F[B]]] = + Eval.defer(inst.fold(fa): + [f[a] <: F[a]] => (F: T[f], fa: f[A]) => + DerivedTraverse.safeTraverse(F)(fa)(f).map(g => G.widen[f[B], F[B]](g)).asInstanceOf[Eval[G[F[B]]]] + ) object Strict: given product[F[_]: ProductInstancesOf[Traverse]]: DerivedTraverse[F] = diff --git a/core/src/test/scala-3/cats/derived/StackSafetySuite.scala b/core/src/test/scala-3/cats/derived/StackSafetySuite.scala new file mode 100644 index 00000000..1dee2560 --- /dev/null +++ b/core/src/test/scala-3/cats/derived/StackSafetySuite.scala @@ -0,0 +1,119 @@ +package cats.derived + +import cats.{Eq, Hash, Invariant, Order, PartialOrder, Show} +import cats.{Bifunctor, Bifoldable, Bitraverse} +import cats.{Foldable, NonEmptyTraverse, Reducible, Traverse} + +object StackSafetySuite: + import ADTs.* + + object eqInst: + given Eq[IList[Int]] = semiauto.eq + + object orderInst: + given Order[IList[Int]] = semiauto.order + + object pOrderInst: + given PartialOrder[IList[Int]] = semiauto.partialOrder + + object hashInst: + given Hash[IList[Int]] = semiauto.hash + + object showInst: + given Show[IList[Int]] = semiauto.show + + object invariantInst: + given Invariant[EnumK1] = semiauto.invariant + + object foldableInst: + given Foldable[EnumK1] = semiauto.foldable + + object traverseInst: + given Traverse[EnumK1] = semiauto.traverse + + object reducibleInst: + given Reducible[EnumK1] = semiauto.reducible + + object netInst: + given NonEmptyTraverse[EnumK1] = semiauto.nonEmptyTraverse + + enum BiTree[+A, +B]: + case Leaf(a: A, b: B) + case Branch(l: BiTree[A, B], r: BiTree[A, B]) + + object BiTree: + given Bifunctor[BiTree] = semiauto.bifunctor + given Bifoldable[BiTree] = semiauto.bifoldable + given Bitraverse[BiTree] = semiauto.bitraverse + +class StackSafetySuite extends KittensSuite: + import ADTs.* + import StackSafetySuite.* + + val Size = 10000 + + def deepIList(): IList[Int] = + (1 to Size).foldLeft[IList[Int]](INil())((acc, i) => ICons(i, acc)) + + def deepEnumK1(): EnumK1[Int] = + (1 to Size).foldLeft[EnumK1[Int]](EnumK1.Leaf(0))((acc, i) => EnumK1.Rec(EnumK1.Leaf(i), acc)) + + def deepBiTree(): BiTree[Int, Int] = + (1 to Size).foldLeft[BiTree[Int, Int]](BiTree.Leaf(0, 0))((acc, i) => BiTree.Branch(BiTree.Leaf(i, i), acc)) + + test("DerivedEq stack safety on IList[Int]"): + import eqInst.given + assert(Eq[IList[Int]].eqv(deepIList(), deepIList())) + + test("DerivedOrder stack safety on IList[Int]"): + import orderInst.given + assertEquals(Order[IList[Int]].compare(deepIList(), deepIList()), 0) + + test("DerivedPartialOrder stack safety on IList[Int]"): + import pOrderInst.given + assertEquals(PartialOrder[IList[Int]].partialCompare(deepIList(), deepIList()), 0.0) + + test("DerivedHash stack safety on IList[Int]"): + import hashInst.given + val h = Hash[IList[Int]].hash(deepIList()) + assert(h != Int.MinValue || h == Int.MinValue) + + test("DerivedShow stack safety on IList[Int]"): + import showInst.given + assert(Show[IList[Int]].show(deepIList()).length > 0) + + test("DerivedInvariant stack safety on EnumK1"): + import invariantInst.given + assert(Invariant[EnumK1].imap(deepEnumK1())(_ + 1)(_ - 1) ne null) + + test("DerivedFoldable.foldLeft stack safety on EnumK1"): + import foldableInst.given + val r = Foldable[EnumK1].foldLeft(deepEnumK1(), 0)(_ + _) + assert(r != Int.MinValue || r == Int.MinValue) + + test("DerivedTraverse stack safety on EnumK1"): + import traverseInst.given + assert(Traverse[EnumK1].map(deepEnumK1())(_ + 1) ne null) + + test("DerivedReducible stack safety on EnumK1"): + import reducibleInst.given + val r = Reducible[EnumK1].reduceLeft(deepEnumK1())(_ + _) + assert(r != Int.MinValue || r == Int.MinValue) + + test("DerivedNonEmptyTraverse stack safety on EnumK1"): + import netInst.given + assert(NonEmptyTraverse[EnumK1].map(deepEnumK1())(_ + 1) ne null) + + test("DerivedBifunctor stack safety on recursive BiTree"): + assert(Bifunctor[BiTree].bimap(deepBiTree())(_ + 1, _ + 1) ne null) + + test("DerivedBifoldable stack safety on recursive BiTree"): + val r = Bifoldable[BiTree].bifoldLeft(deepBiTree(), 0)(_ + _, _ + _) + assert(r != Int.MinValue || r == Int.MinValue) + + test("DerivedBitraverse stack safety on recursive BiTree"): + import cats.Eval + val r = Bitraverse[BiTree].bitraverse[Eval, Int, Int, Int, Int](deepBiTree())(Eval.now(_), Eval.now(_)).value + assert(r ne null) + +end StackSafetySuite From 1622c0490a631a8ae0465e0e9cce0745b0b6fdc5 Mon Sep 17 00:00:00 2001 From: ismat Date: Tue, 16 Jun 2026 15:53:14 +0300 Subject: [PATCH 3/3] Restore fast direct-recursion derivation by default; add opt-in stack-safe variants Default derivation (semiauto / *.derived / strict) again uses direct recursion, removing the ~5-9x performance regression introduced when the derivations were trampolined through Eval. Stack-safe (Eval-trampolined) derivation is now opt-in via cats.derived.stackSafe.* for Eq, Hash, Order, PartialOrder, Show, Functor, Contravariant, Invariant, Foldable, Reducible, Traverse and NonEmptyTraverse. Bifunctor, Bifoldable and Bitraverse are reverted to fast direct recursion (no Eval variant). Stack-safety tests updated to use the opt-in stackSafe entry points. --- .../cats/derived/DerivedBifoldable.scala | 39 ++------ .../cats/derived/DerivedBifunctor.scala | 44 ++------- .../cats/derived/DerivedBitraverse.scala | 63 ++++-------- .../cats/derived/DerivedContravariant.scala | 48 ++++++--- .../main/scala-3/cats/derived/DerivedEq.scala | 42 ++++++-- .../cats/derived/DerivedFoldable.scala | 54 +++++++++-- .../scala-3/cats/derived/DerivedFunctor.scala | 48 ++++++--- .../scala-3/cats/derived/DerivedHash.scala | 55 +++++++++-- .../cats/derived/DerivedInvariant.scala | 48 ++++++--- .../derived/DerivedNonEmptyTraverse.scala | 97 ++++++++++++++----- .../scala-3/cats/derived/DerivedOrder.scala | 48 +++++++-- .../cats/derived/DerivedPartialOrder.scala | 48 +++++++-- .../cats/derived/DerivedReducible.scala | 89 ++++++++++++++--- .../scala-3/cats/derived/DerivedShow.scala | 66 +++++++++++-- .../cats/derived/DerivedTraverse.scala | 67 ++++++++++--- .../main/scala-3/cats/derived/package.scala | 31 ++++++ .../cats/derived/ContravariantSuite.scala | 9 +- .../scala-3/cats/derived/FunctorSuite.scala | 7 +- .../cats/derived/StackSafetySuite.scala | 45 ++------- 19 files changed, 646 insertions(+), 302 deletions(-) diff --git a/core/src/main/scala-3/cats/derived/DerivedBifoldable.scala b/core/src/main/scala-3/cats/derived/DerivedBifoldable.scala index d68917e1..9330f2cd 100644 --- a/core/src/main/scala-3/cats/derived/DerivedBifoldable.scala +++ b/core/src/main/scala-3/cats/derived/DerivedBifoldable.scala @@ -74,30 +74,9 @@ object DerivedBifoldable: given [F[_, _]](using => CoproductInstances[Bifoldable |: Derived, F]): DerivedBifoldable[F] = Strict.coproduct - private[derived] trait Safe[F[_, _]] extends Bifoldable[F]: - private[derived] def safeBifoldLeft[A, B, C](fab: F[A, B], c: Eval[C])( - f: (C, A) => C, - g: (C, B) => C - ): Eval[C] - override def bifoldLeft[A, B, C](fab: F[A, B], c: C)(f: (C, A) => C, g: (C, B) => C): C = - safeBifoldLeft(fab, Eval.now(c))(f, g).value - - private[derived] def safeBifoldLeft[F[_, _], A, B, C](F: Bifoldable[F])(fab: F[A, B], c: Eval[C])( - f: (C, A) => C, - g: (C, B) => C - ): Eval[C] = - F match - case safe: Safe[F] @scala.unchecked => safe.safeBifoldLeft(fab, c)(f, g) - case _ => c.map(F.bifoldLeft(fab, _)(f, g)) - - trait Product[T[f[_, _]] <: Bifoldable[f], F[_, _]](using inst: ProductInstances[T, F]) extends Safe[F]: - private[derived] final override def safeBifoldLeft[A, B, C](fab: F[A, B], c: Eval[C])( - f: (C, A) => C, - g: (C, B) => C - ): Eval[C] = - inst.foldLeft[A, B, Eval[C]](fab)(c): - [f[_, _]] => (acc: Eval[C], F: T[f], fab: f[A, B]) => - DerivedBifoldable.safeBifoldLeft(F)(fab, acc)(f, g) + trait Product[T[f[_, _]] <: Bifoldable[f], F[_, _]](using inst: ProductInstances[T, F]) extends Bifoldable[F]: + final override def bifoldLeft[A, B, C](fab: F[A, B], c: C)(f: (C, A) => C, g: (C, B) => C): C = + inst.foldLeft(fab)(c)([f[_, _]] => (c: C, F: T[f], fab: f[A, B]) => F.bifoldLeft(fab, c)(f, g)) final override def bifoldRight[A, B, C](fab: F[A, B], c: Eval[C])( f: (A, Eval[C]) => Eval[C], @@ -105,15 +84,9 @@ object DerivedBifoldable: ): Eval[C] = inst.foldRight(fab)(c): [f[_, _]] => (F: T[f], fab: f[A, B], c: Eval[C]) => Eval.defer(F.bifoldRight(fab, c)(f, g)) - trait Coproduct[T[f[_, _]] <: Bifoldable[f], F[_, _]](using inst: CoproductInstances[T, F]) extends Safe[F]: - private[derived] final override def safeBifoldLeft[A, B, C](fab: F[A, B], c: Eval[C])( - f: (C, A) => C, - g: (C, B) => C - ): Eval[C] = - Eval.defer(inst.fold(fab): - [f[_, _]] => (F: T[f], fab: f[A, B]) => - DerivedBifoldable.safeBifoldLeft(F)(fab, c)(f, g) - ) + trait Coproduct[T[f[_, _]] <: Bifoldable[f], F[_, _]](using inst: CoproductInstances[T, F]) extends Bifoldable[F]: + final override def bifoldLeft[A, B, C](fab: F[A, B], c: C)(f: (C, A) => C, g: (C, B) => C): C = + inst.fold(fab)([f[_, _]] => (F: T[f], fab: f[A, B]) => F.bifoldLeft(fab, c)(f, g)) final override def bifoldRight[A, B, C](fab: F[A, B], c: Eval[C])( f: (A, Eval[C]) => Eval[C], diff --git a/core/src/main/scala-3/cats/derived/DerivedBifunctor.scala b/core/src/main/scala-3/cats/derived/DerivedBifunctor.scala index cb196251..b75653c2 100644 --- a/core/src/main/scala-3/cats/derived/DerivedBifunctor.scala +++ b/core/src/main/scala-3/cats/derived/DerivedBifunctor.scala @@ -1,6 +1,6 @@ package cats.derived -import cats.{Bifunctor, Eval, Functor} +import cats.{Bifunctor, Functor} import shapeless3.deriving.Derived import shapeless3.deriving.K2.* @@ -46,47 +46,17 @@ object DerivedBifunctor: new Lazy(() => F.unify.compose(using G.unify)) with Bifunctor[[a, b] =>> F[G[a, b], G[a, b]]]: export delegate.* - given [F[_, _]](using inst: ProductInstances[Bifunctor |: Derived, F]): DerivedBifunctor[F] = - Strict.product(using inst.unify) + given generic[F[_, _]](using inst: => Instances[Bifunctor |: Derived, F]): DerivedBifunctor[F] = + gen(using inst.unify) - given [F[_, _]](using => CoproductInstances[Bifunctor |: Derived, F]): DerivedBifunctor[F] = - Strict.coproduct - - private[derived] trait Safe[F[_, _]] extends Bifunctor[F]: - private[derived] def safeBimap[A, B, C, D](fab: F[A, B])(f: A => C, g: B => D): Eval[F[C, D]] - override def bimap[A, B, C, D](fab: F[A, B])(f: A => C, g: B => D): F[C, D] = safeBimap(fab)(f, g).value - - private[derived] def safeBimap[F[_, _], A, B, C, D]( - F: Bifunctor[F] - )(fab: F[A, B])(f: A => C, g: B => D): Eval[F[C, D]] = - F match - case safe: Safe[F] @scala.unchecked => safe.safeBimap(fab)(f, g) - case _ => Eval.later(F.bimap(fab)(f, g)) + private def gen[F[_, _]: InstancesOf[Bifunctor]]: DerivedBifunctor[F] = + new Generic[Bifunctor, F] {} trait Generic[T[f[_, _]] <: Bifunctor[f], F[_, _]](using inst: Instances[T, F]) extends Bifunctor[F]: final override def bimap[A, B, C, D](fab: F[A, B])(f: A => C, g: B => D): F[C, D] = inst.map(fab)([f[_, _]] => (F: T[f], fa: f[A, B]) => F.bimap(fa)(f, g)) - trait Product[T[f[_, _]] <: Bifunctor[f], F[_, _]](using inst: ProductInstances[T, F]) extends Safe[F]: - private[derived] final override def safeBimap[A, B, C, D](fab: F[A, B])(f: A => C, g: B => D): Eval[F[C, D]] = - val pure = [a] => (x: a) => Eval.now(x) - val mp = [a, b] => (ea: Eval[a], h: a => b) => ea.map(h) - val ap = [a, b] => (ef: Eval[a => b], ea: Eval[a]) => ef.flatMap(h => ea.map(h)) - inst.traverse[A, B, Eval, C, D](fab)(mp)(pure)(ap)( - [f[_, _]] => (F: T[f], fa: f[A, B]) => DerivedBifunctor.safeBimap(F)(fa)(f, g) - ) - - trait Coproduct[T[f[_, _]] <: Bifunctor[f], F[_, _]](using inst: CoproductInstances[T, F]) extends Safe[F]: - private[derived] final override def safeBimap[A, B, C, D](fab: F[A, B])(f: A => C, g: B => D): Eval[F[C, D]] = - Eval.defer(inst.fold(fab): - [f[a, b] <: F[a, b]] => (F: T[f], fa: f[A, B]) => - DerivedBifunctor.safeBimap(F)(fa)(f, g).asInstanceOf[Eval[F[C, D]]] - ) - object Strict: - given product[F[_, _]: ProductInstancesOf[Bifunctor]]: DerivedBifunctor[F] = - new Product[Bifunctor, F] {} - + given product[F[_, _]: ProductInstancesOf[Bifunctor]]: DerivedBifunctor[F] = gen given coproduct[F[_, _]](using inst: => CoproductInstances[Bifunctor |: Derived, F]): DerivedBifunctor[F] = - given CoproductInstances[Bifunctor, F] = inst.unify - new Coproduct[Bifunctor, F] {} + gen(using inst.unify) diff --git a/core/src/main/scala-3/cats/derived/DerivedBitraverse.scala b/core/src/main/scala-3/cats/derived/DerivedBitraverse.scala index cc98164a..bf108691 100644 --- a/core/src/main/scala-3/cats/derived/DerivedBitraverse.scala +++ b/core/src/main/scala-3/cats/derived/DerivedBitraverse.scala @@ -88,54 +88,33 @@ object DerivedBitraverse: given [F[_, _]](using => CoproductInstances[Bitraverse |: Derived, F]): DerivedBitraverse[F] = Strict.coproduct - private[derived] trait Safe[F[_, _]] extends Bitraverse[F]: - private[derived] def safeBitraverse[G[_], A, B, C, D](fab: F[A, B])(f: A => G[C], g: B => G[D])(using - G: Applicative[G] - ): Eval[G[F[C, D]]] - override def bitraverse[G[_], A, B, C, D](fab: F[A, B])(f: A => G[C], g: B => G[D])(using - G: Applicative[G] - ): G[F[C, D]] = safeBitraverse(fab)(f, g).value - - private[derived] def safeBitraverse[F[_, _], G[_], A, B, C, D]( - F: Bitraverse[F] - )(fab: F[A, B])(f: A => G[C], g: B => G[D])(using G: Applicative[G]): Eval[G[F[C, D]]] = - F match - case safe: Safe[F] @scala.unchecked => safe.safeBitraverse(fab)(f, g) - case _ => Eval.later(F.bitraverse(fab)(f, g)) - trait Product[T[f[_, _]] <: Bitraverse[f], F[_, _]](using inst: ProductInstances[T, F]) - extends Safe[F], - DerivedBifunctor.Product[T, F], - DerivedBifoldable.Product[T, F]: - - private[derived] final override def safeBitraverse[G[_], A, B, C, D]( - fab: F[A, B] - )(f: A => G[C], g: B => G[D])(using G: Applicative[G]): Eval[G[F[C, D]]] = - val pure = [a] => (x: a) => Eval.now(G.pure(x)) - val mp = [a, b] => (ega: Eval[G[a]], h: a => b) => ega.map(ga => G.map(ga)(h)) - val ap = [a, b] => (egf: Eval[G[a => b]], ega: Eval[G[a]]) => - egf.flatMap(gf => ega.map(ga => G.ap(gf)(ga))) - inst.traverse[A, B, [x] =>> Eval[G[x]], C, D](fab)(mp)(pure)(ap)( - [f[_, _]] => (F: T[f], fab: f[A, B]) => DerivedBitraverse.safeBitraverse(F)(fab)(f, g) - ) + extends Bitraverse[F] + with DerivedBifunctor.Generic[T, F] + with DerivedBifoldable.Product[T, F]: + + final override def bitraverse[G[_], A, B, C, D](fab: F[A, B])(f: A => G[C], g: B => G[D])(using + G: Applicative[G] + ): G[F[C, D]] = + val pure = [a] => (x: a) => G.pure(x) + val map = [a, b] => (ga: G[a], f: a => b) => G.map(ga)(f) + val ap = [a, b] => (gf: G[a => b], ga: G[a]) => G.ap(gf)(ga) + inst.traverse[A, B, G, C, D](fab)(map)(pure)(ap)([f[_, _]] => (F: T[f], fab: f[A, B]) => F.bitraverse(fab)(f, g)) trait Coproduct[T[f[_, _]] <: Bitraverse[f], F[_, _]](using inst: CoproductInstances[T, F]) - extends Safe[F], - DerivedBifunctor.Coproduct[T, F], - DerivedBifoldable.Coproduct[T, F]: - - private[derived] final override def safeBitraverse[G[_], A, B, C, D]( - fab: F[A, B] - )(f: A => G[C], g: B => G[D])(using G: Applicative[G]): Eval[G[F[C, D]]] = - Eval.defer(inst.fold(fab): - [f[a, b] <: F[a, b]] => (F: T[f], fa: f[A, B]) => - DerivedBitraverse.safeBitraverse(F)(fa)(f, g).map(g0 => G.widen[f[C, D], F[C, D]](g0)).asInstanceOf[Eval[G[F[C, D]]]] - ) + extends Bitraverse[F] + with DerivedBifunctor.Generic[T, F] + with DerivedBifoldable.Coproduct[T, F]: + + final override def bitraverse[G[_], A, B, C, D](fab: F[A, B])(f: A => G[C], g: B => G[D])(using + G: Applicative[G] + ): G[F[C, D]] = inst.fold(fab): + [f[a, b] <: F[a, b]] => (F: T[f], fa: f[A, B]) => G.widen[f[C, D], F[C, D]](F.bitraverse(fa)(f, g)) object Strict: given product[F[_, _]: ProductInstancesOf[Bitraverse]]: DerivedBitraverse[F] = - new Product[Bitraverse, F] {} + new Bitraverse[F] with Product[Bitraverse, F] {} given coproduct[F[_, _]](using inst: => CoproductInstances[Bitraverse |: Derived, F]): DerivedBitraverse[F] = given CoproductInstances[Bitraverse, F] = inst.unify - new Coproduct[Bitraverse, F] {} + new Bitraverse[F] with Coproduct[Bitraverse, F] {} diff --git a/core/src/main/scala-3/cats/derived/DerivedContravariant.scala b/core/src/main/scala-3/cats/derived/DerivedContravariant.scala index 3c637792..cd517cec 100644 --- a/core/src/main/scala-3/cats/derived/DerivedContravariant.scala +++ b/core/src/main/scala-3/cats/derived/DerivedContravariant.scala @@ -24,6 +24,13 @@ object DerivedContravariant: import Strict.given summonInline[DerivedContravariant[F]].instance + /** Stack-safe (trampolined via [[cats.Eval]]) derivation. Opt-in: slower on shallow data, but does not overflow the + * stack on deeply nested recursive ADTs. + */ + inline def stackSafe[F[_]]: Contravariant[F] = + import StackSafe.given + summonInline[DerivedContravariant[F]].instance + given [T]: DerivedContravariant[Const[T]] = new Contravariant[Const[T]]: def contramap[A, B](fa: T)(f: B => A): T = fa @@ -44,6 +51,31 @@ object DerivedContravariant: protected given [F[_]: Functor |: Derived, G[_]: Contravariant |: Derived]: DerivedContravariant[[x] =>> F[G[x]]] = nested + // ---- Default: fast direct recursion ---- + + private def generic[F[_]: InstancesOf[Contravariant]]: DerivedContravariant[F] = + new Generic[Contravariant, F] {} + + trait Generic[T[f[_]] <: Contravariant[f], F[_]](using inst: Instances[T, F]) extends Contravariant[F]: + final override def contramap[A, B](fa: F[A])(f: B => A): F[B] = + inst.map(fa)([f[_]] => (T: T[f], fa: f[A]) => T.contramap(fa)(f)) + + object Strict: + given product[F[_]: ProductInstancesOf[Contravariant]]: DerivedContravariant[F] = generic + given coproduct[F[_]](using inst: => CoproductInstances[Contravariant |: Derived, F]): DerivedContravariant[F] = + generic(using inst.unify) + + // ---- Opt-in: stack-safe recursion via Eval ---- + + object StackSafe: + given product[F[_]](using inst: ProductInstances[Contravariant |: Derived, F]): DerivedContravariant[F] = + given ProductInstances[Contravariant, F] = inst.unify + new SafeProduct[Contravariant, F] {} + + given coproduct[F[_]](using inst: => CoproductInstances[Contravariant |: Derived, F]): DerivedContravariant[F] = + given CoproductInstances[Contravariant, F] = inst.unify + new SafeCoproduct[Contravariant, F] {} + private[derived] trait Safe[F[_]] extends Contravariant[F]: private[derived] def safeContramap[A, B](fa: F[A])(f: B => A): Eval[F[B]] override def contramap[A, B](fa: F[A])(f: B => A): F[B] = safeContramap(fa)(f).value @@ -53,11 +85,7 @@ object DerivedContravariant: case safe: Safe[F] @scala.unchecked => safe.safeContramap(fa)(f) case _ => Eval.later(F.contramap(fa)(f)) - trait Generic[T[f[_]] <: Contravariant[f], F[_]](using inst: Instances[T, F]) extends Contravariant[F]: - final override def contramap[A, B](fa: F[A])(f: B => A): F[B] = - inst.map(fa)([f[_]] => (T: T[f], fa: f[A]) => T.contramap(fa)(f)) - - trait Product[T[f[_]] <: Contravariant[f], F[_]](using inst: ProductInstances[T, F]) extends Safe[F]: + trait SafeProduct[T[f[_]] <: Contravariant[f], F[_]](using inst: ProductInstances[T, F]) extends Safe[F]: private[derived] final override def safeContramap[A, B](fa: F[A])(f: B => A): Eval[F[B]] = val pure = [a] => (x: a) => Eval.now(x) val mp = [a, b] => (ea: Eval[a], g: a => b) => ea.map(g) @@ -66,17 +94,9 @@ object DerivedContravariant: [f[_]] => (F: T[f], fa: f[A]) => DerivedContravariant.safeContramap(F)(fa)(f) ) - trait Coproduct[T[f[_]] <: Contravariant[f], F[_]](using inst: CoproductInstances[T, F]) extends Safe[F]: + trait SafeCoproduct[T[f[_]] <: Contravariant[f], F[_]](using inst: CoproductInstances[T, F]) extends Safe[F]: private[derived] final override def safeContramap[A, B](fa: F[A])(f: B => A): Eval[F[B]] = Eval.defer(inst.fold(fa)( [f[a] <: F[a]] => (F: T[f], fa: f[A]) => DerivedContravariant.safeContramap(F)(fa)(f).asInstanceOf[Eval[F[B]]] )) - - object Strict: - given product[F[_]: ProductInstancesOf[Contravariant]]: DerivedContravariant[F] = - new Product[Contravariant, F] {} - - given coproduct[F[_]](using inst: => CoproductInstances[Contravariant |: Derived, F]): DerivedContravariant[F] = - given CoproductInstances[Contravariant, F] = inst.unify - new Coproduct[Contravariant, F] {} diff --git a/core/src/main/scala-3/cats/derived/DerivedEq.scala b/core/src/main/scala-3/cats/derived/DerivedEq.scala index 2f926366..9be56ee1 100644 --- a/core/src/main/scala-3/cats/derived/DerivedEq.scala +++ b/core/src/main/scala-3/cats/derived/DerivedEq.scala @@ -22,6 +22,13 @@ object DerivedEq: import Strict.given summonInline[DerivedEq[A]].instance + /** Stack-safe (trampolined via [[cats.Eval]]) derivation. Opt-in: slower on shallow data, but does not overflow the + * stack on deeply nested recursive ADTs. + */ + inline def stackSafe[A]: Eq[A] = + import StackSafe.given + summonInline[DerivedEq[A]].instance + @unused given singleton[A <: Singleton: ValueOf]: DerivedEq[A] = Eq.allEqual @@ -33,6 +40,32 @@ object DerivedEq: given CoproductInstances[Eq, A] = inst.unify new Coproduct[Eq, A] {} + // ---- Default: fast direct recursion ---- + + trait Product[F[x] <: Eq[x], A](using inst: ProductInstances[F, A]) extends Eq[A]: + final override 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)(acc) + + trait Coproduct[F[x] <: Eq[x], A](using inst: CoproductInstances[F, A]) extends Eq[A]: + final override def eqv(x: A, y: A): Boolean = inst.fold2(x, y)(false): + [t] => (eqt: F[t], x: t, y: t) => eqt.eqv(x, y) + + object Strict: + export DerivedEq.coproduct + given product[A: ProductInstancesOf[Eq]]: DerivedEq[A] = + new Product[Eq, A] {} + + // ---- Opt-in: stack-safe recursion via Eval ---- + + object StackSafe: + given product[A](using inst: => ProductInstances[Eq |: Derived, A]): DerivedEq[A] = + given ProductInstances[Eq, A] = inst.unify + new SafeProduct[Eq, A] {} + + given coproduct[A](using inst: => CoproductInstances[Eq |: Derived, A]): DerivedEq[A] = + given CoproductInstances[Eq, A] = inst.unify + new SafeCoproduct[Eq, A] {} + private[derived] trait Safe[A] extends Eq[A]: private[derived] def safeEqv(x: A, y: A): Eval[Boolean] override def eqv(x: A, y: A): Boolean = safeEqv(x, y).value @@ -42,7 +75,7 @@ object DerivedEq: case safe: Safe[?] => safe.asInstanceOf[Safe[A]].safeEqv(x, y) case _ => Eval.later(F.eqv(x, y)) - trait Product[F[x] <: Eq[x], A](using inst: ProductInstances[F, A]) extends Safe[A]: + trait SafeProduct[F[x] <: Eq[x], A](using inst: ProductInstances[F, A]) extends Safe[A]: private[derived] final override def safeEqv(x: A, y: A): Eval[Boolean] = inst.foldLeft2[Eval[Boolean]](x, y)(Eval.now(true)): [t] => (acc: Eval[Boolean], eqt: F[t], xt: t, yt: t) => @@ -50,13 +83,8 @@ object DerivedEq: if !b then Eval.now(false) else DerivedEq.safeEqv(eqt)(xt, yt) Complete(false)(next)(next) - trait Coproduct[F[x] <: Eq[x], A](using inst: CoproductInstances[F, A]) extends Safe[A]: + trait SafeCoproduct[F[x] <: Eq[x], A](using inst: CoproductInstances[F, A]) extends Safe[A]: private[derived] final override def safeEqv(x: A, y: A): Eval[Boolean] = Eval.defer(inst.fold2(x, y)(Eval.now(false): Eval[Boolean]): [t] => (eqt: F[t], xt: t, yt: t) => DerivedEq.safeEqv(eqt)(xt, yt) ) - - object Strict: - export DerivedEq.coproduct - given product[A: ProductInstancesOf[Eq]]: DerivedEq[A] = - new Product[Eq, A] {} diff --git a/core/src/main/scala-3/cats/derived/DerivedFoldable.scala b/core/src/main/scala-3/cats/derived/DerivedFoldable.scala index 785387f8..c2afef80 100644 --- a/core/src/main/scala-3/cats/derived/DerivedFoldable.scala +++ b/core/src/main/scala-3/cats/derived/DerivedFoldable.scala @@ -24,6 +24,13 @@ object DerivedFoldable: import Strict.given summonInline[DerivedFoldable[F]].instance + /** Stack-safe (trampolined via [[cats.Eval]]) derivation. Opt-in: slower on shallow data, but does not overflow the + * stack on deeply nested recursive ADTs. + */ + inline def stackSafe[F[_]]: Foldable[F] = + import StackSafe.given + summonInline[DerivedFoldable[F]].instance + given [T]: DerivedFoldable[Const[T]] = new Foldable[Const[T]]: def foldLeft[A, B](fa: T, b: B)(f: (B, A) => B): B = b def foldRight[A, B](fa: T, lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = lb @@ -44,6 +51,41 @@ object DerivedFoldable: @deprecated("Kept for binary compatibility", "3.2.0") protected given [F[_]: Foldable |: Derived, G[_]: Foldable |: Derived]: DerivedFoldable[[x] =>> F[G[x]]] = nested + // ---- Default: fast direct recursion ---- + + trait Product[T[f[_]] <: Foldable[f], F[_]](using inst: ProductInstances[T, F]) extends Foldable[F]: + final override def foldLeft[A, B](fa: F[A], b: B)(f: (B, A) => B): B = + inst.foldLeft(fa)(b)([f[_]] => (b: B, F: T[f], fa: f[A]) => F.foldLeft(fa, b)(f)) + + final override def foldRight[A, B](fa: F[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = + inst.foldRight(fa)(lb)([f[_]] => (F: T[f], fa: f[A], lb: Eval[B]) => Eval.defer(F.foldRight(fa, lb)(f))) + + trait Coproduct[T[f[_]] <: Foldable[f], F[_]](using inst: CoproductInstances[T, F]) extends Foldable[F]: + final override def foldLeft[A, B](fa: F[A], b: B)(f: (B, A) => B): B = + inst.fold(fa)([f[_]] => (F: T[f], fa: f[A]) => F.foldLeft(fa, b)(f)) + + final override def foldRight[A, B](fa: F[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = + inst.fold(fa)([f[_]] => (F: T[f], fa: f[A]) => Eval.defer(F.foldRight(fa, lb)(f))) + + object Strict: + given product[F[_]: ProductInstancesOf[Foldable]]: DerivedFoldable[F] = + new Product[Foldable, F] {} + + given coproduct[F[_]](using inst: => CoproductInstances[Foldable |: Derived, F]): DerivedFoldable[F] = + given CoproductInstances[Foldable, F] = inst.unify + new Coproduct[Foldable, F] {} + + // ---- Opt-in: stack-safe recursion via Eval ---- + + object StackSafe: + given product[F[_]](using inst: ProductInstances[Foldable |: Derived, F]): DerivedFoldable[F] = + given ProductInstances[Foldable, F] = inst.unify + new SafeProduct[Foldable, F] {} + + given coproduct[F[_]](using inst: => CoproductInstances[Foldable |: Derived, F]): DerivedFoldable[F] = + given CoproductInstances[Foldable, F] = inst.unify + new SafeCoproduct[Foldable, F] {} + private[derived] trait Safe[F[_]] extends Foldable[F]: private[derived] def safeFoldLeft[A, B](fa: F[A], b: Eval[B])(f: (B, A) => B): Eval[B] override def foldLeft[A, B](fa: F[A], b: B)(f: (B, A) => B): B = @@ -54,7 +96,7 @@ object DerivedFoldable: case safe: Safe[F] @scala.unchecked => safe.safeFoldLeft(fa, b)(f) case _ => b.map(F.foldLeft(fa, _)(f)) - trait Product[T[f[_]] <: Foldable[f], F[_]](using inst: ProductInstances[T, F]) extends Safe[F]: + trait SafeProduct[T[f[_]] <: Foldable[f], F[_]](using inst: ProductInstances[T, F]) extends Safe[F]: private[derived] final override def safeFoldLeft[A, B](fa: F[A], b: Eval[B])(f: (B, A) => B): Eval[B] = inst.foldLeft[A, Eval[B]](fa)(b): [f[_]] => (acc: Eval[B], F: T[f], fa: f[A]) => @@ -63,7 +105,7 @@ object DerivedFoldable: final override def foldRight[A, B](fa: F[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = inst.foldRight(fa)(lb)([f[_]] => (F: T[f], fa: f[A], lb: Eval[B]) => Eval.defer(F.foldRight(fa, lb)(f))) - trait Coproduct[T[f[_]] <: Foldable[f], F[_]](using inst: CoproductInstances[T, F]) extends Safe[F]: + trait SafeCoproduct[T[f[_]] <: Foldable[f], F[_]](using inst: CoproductInstances[T, F]) extends Safe[F]: private[derived] final override def safeFoldLeft[A, B](fa: F[A], b: Eval[B])(f: (B, A) => B): Eval[B] = Eval.defer(inst.fold(fa)([f[_]] => (F: T[f], fa: f[A]) => DerivedFoldable.safeFoldLeft(F)(fa, b)(f) @@ -71,11 +113,3 @@ object DerivedFoldable: final override def foldRight[A, B](fa: F[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = inst.fold(fa)([f[_]] => (F: T[f], fa: f[A]) => Eval.defer(F.foldRight(fa, lb)(f))) - - object Strict: - given product[F[_]: ProductInstancesOf[Foldable]]: DerivedFoldable[F] = - new Product[Foldable, F] {} - - given coproduct[F[_]](using inst: => CoproductInstances[Foldable |: Derived, F]): DerivedFoldable[F] = - given CoproductInstances[Foldable, F] = inst.unify - new Coproduct[Foldable, F] {} diff --git a/core/src/main/scala-3/cats/derived/DerivedFunctor.scala b/core/src/main/scala-3/cats/derived/DerivedFunctor.scala index 53673abb..0ad74fdb 100644 --- a/core/src/main/scala-3/cats/derived/DerivedFunctor.scala +++ b/core/src/main/scala-3/cats/derived/DerivedFunctor.scala @@ -25,6 +25,13 @@ object DerivedFunctor: import Strict.given summonInline[DerivedFunctor[F]].instance + /** Stack-safe (trampolined via [[cats.Eval]]) derivation. Opt-in: slower on shallow data, but does not overflow the + * stack on deeply nested recursive ADTs. + */ + inline def stackSafe[F[_]]: Functor[F] = + import StackSafe.given + summonInline[DerivedFunctor[F]].instance + given [T]: DerivedFunctor[Const[T]] = new Functor[Const[T]]: def map[A, B](fa: T)(f: A => B): T = fa @@ -61,6 +68,31 @@ object DerivedFunctor: ): DerivedFunctor[[x] =>> F[G[x]]] = nested(using F, G) + // ---- Default: fast direct recursion ---- + + private def generic[F[_]: InstancesOf[Functor]]: DerivedFunctor[F] = + new Generic[Functor, F] {} + + trait Generic[T[f[_]] <: Functor[f], F[_]](using inst: Instances[T, F]) extends Functor[F]: + final override def map[A, B](fa: F[A])(f: A => B): F[B] = + inst.map(fa)([f[_]] => (F: T[f], fa: f[A]) => F.map(fa)(f)) + + object Strict: + given product[F[_]: ProductInstancesOf[Functor]]: DerivedFunctor[F] = generic + given coproduct[F[_]](using inst: => CoproductInstances[Functor |: Derived, F]): DerivedFunctor[F] = + generic(using inst.unify) + + // ---- Opt-in: stack-safe recursion via Eval ---- + + object StackSafe: + given product[F[_]](using inst: ProductInstances[Functor |: Derived, F]): DerivedFunctor[F] = + given ProductInstances[Functor, F] = inst.unify + new SafeProduct[Functor, F] {} + + given coproduct[F[_]](using inst: => CoproductInstances[Functor |: Derived, F]): DerivedFunctor[F] = + given CoproductInstances[Functor, F] = inst.unify + new SafeCoproduct[Functor, F] {} + private[derived] trait Safe[F[_]] extends Functor[F]: private[derived] def safeMap[A, B](fa: F[A])(f: A => B): Eval[F[B]] override def map[A, B](fa: F[A])(f: A => B): F[B] = safeMap(fa)(f).value @@ -70,11 +102,7 @@ object DerivedFunctor: case safe: Safe[F] @scala.unchecked => safe.safeMap(fa)(f) case _ => Eval.later(F.map(fa)(f)) - trait Generic[T[f[_]] <: Functor[f], F[_]](using inst: Instances[T, F]) extends Functor[F]: - final override def map[A, B](fa: F[A])(f: A => B): F[B] = - inst.map(fa)([f[_]] => (F: T[f], fa: f[A]) => F.map(fa)(f)) - - trait Product[T[f[_]] <: Functor[f], F[_]](using inst: ProductInstances[T, F]) extends Safe[F]: + trait SafeProduct[T[f[_]] <: Functor[f], F[_]](using inst: ProductInstances[T, F]) extends Safe[F]: private[derived] final override def safeMap[A, B](fa: F[A])(f: A => B): Eval[F[B]] = val pure = [a] => (x: a) => Eval.now(x) val mp = [a, b] => (ea: Eval[a], g: a => b) => ea.map(g) @@ -83,17 +111,9 @@ object DerivedFunctor: [f[_]] => (F: T[f], fa: f[A]) => DerivedFunctor.safeMap(F)(fa)(f) ) - trait Coproduct[T[f[_]] <: Functor[f], F[_]](using inst: CoproductInstances[T, F]) extends Safe[F]: + trait SafeCoproduct[T[f[_]] <: Functor[f], F[_]](using inst: CoproductInstances[T, F]) extends Safe[F]: private[derived] final override def safeMap[A, B](fa: F[A])(f: A => B): Eval[F[B]] = Eval.defer(inst.fold(fa)( [f[a] <: F[a]] => (F: T[f], fa: f[A]) => DerivedFunctor.safeMap(F)(fa)(f).asInstanceOf[Eval[F[B]]] )) - - object Strict: - given product[F[_]: ProductInstancesOf[Functor]]: DerivedFunctor[F] = - new Product[Functor, F] {} - - given coproduct[F[_]](using inst: => CoproductInstances[Functor |: Derived, F]): DerivedFunctor[F] = - given CoproductInstances[Functor, F] = inst.unify - new Coproduct[Functor, F] {} diff --git a/core/src/main/scala-3/cats/derived/DerivedHash.scala b/core/src/main/scala-3/cats/derived/DerivedHash.scala index b83248b3..494cea2a 100644 --- a/core/src/main/scala-3/cats/derived/DerivedHash.scala +++ b/core/src/main/scala-3/cats/derived/DerivedHash.scala @@ -23,6 +23,14 @@ object DerivedHash: import Strict.given summonInline[DerivedHash[A]].instance + /** Stack-safe (trampolined via [[cats.Eval]]) derivation. Opt-in: slower on shallow data, but does not overflow the + * stack on deeply nested recursive ADTs. + */ + inline def stackSafe[A]: Hash[A] = + import StackSafe.given + summonInline[DerivedHash[A]].instance + + // These instances support singleton types unlike the instances in Cats' kernel. given boolean[A <: Boolean]: DerivedHash[A] = Hash.fromUniversalHashCode given byte[A <: Byte]: DerivedHash[A] = Hash.fromUniversalHashCode given short[A <: Short]: DerivedHash[A] = Hash.fromUniversalHashCode @@ -41,6 +49,40 @@ object DerivedHash: given CoproductInstances[Hash, A] = inst.unify new Coproduct[Hash, A] {} + // ---- Default: fast direct recursion ---- + + trait Product[F[x] <: Hash[x], A <: scala.Product](using inst: ProductInstances[F, A]) + extends DerivedEq.Product[F, A], + Hash[A]: + + final override def hash(x: A): Int = + val arity = x.productArity + val prefix = x.productPrefix.hashCode + if arity <= 0 then prefix + else + val hash = inst.foldLeft[Int](x)(MurmurHash3.mix(MurmurHash3.productSeed, prefix)): + [t] => (acc: Int, h: F[t], x: t) => MurmurHash3.mix(acc, h.hash(x)) + MurmurHash3.finalizeHash(hash, arity) + + trait Coproduct[F[x] <: Hash[x], A](using inst: CoproductInstances[F, A]) extends DerivedEq.Coproduct[F, A], Hash[A]: + final override def hash(x: A): Int = inst.fold[Int](x)([t] => (h: F[t], x: t) => h.hash(x)) + + object Strict: + export DerivedHash.coproduct + given product[A <: scala.Product: ProductInstancesOf[Hash]]: DerivedHash[A] = + new Product[Hash, A] {} + + // ---- Opt-in: stack-safe recursion via Eval ---- + + object StackSafe: + given product[A <: scala.Product](using inst: => ProductInstances[Hash |: Derived, A]): DerivedHash[A] = + given ProductInstances[Hash, A] = inst.unify + new SafeProduct[Hash, A] {} + + given coproduct[A](using inst: => CoproductInstances[Hash |: Derived, A]): DerivedHash[A] = + given CoproductInstances[Hash, A] = inst.unify + new SafeCoproduct[Hash, A] {} + private[derived] trait Safe[A] extends Hash[A]: private[derived] def safeHash(x: A): Eval[Int] override def hash(x: A): Int = safeHash(x).value @@ -50,8 +92,8 @@ object DerivedHash: case safe: Safe[?] => safe.asInstanceOf[Safe[A]].safeHash(x) case _ => Eval.later(F.hash(x)) - trait Product[F[x] <: Hash[x], A <: scala.Product](using inst: ProductInstances[F, A]) - extends DerivedEq.Product[F, A], + trait SafeProduct[F[x] <: Hash[x], A <: scala.Product](using inst: ProductInstances[F, A]) + extends DerivedEq.SafeProduct[F, A], Safe[A]: private[derived] final override def safeHash(x: A): Eval[Int] = @@ -65,11 +107,8 @@ object DerivedHash: acc.flatMap(a => DerivedHash.safeHash(h)(xt).map(hh => MurmurHash3.mix(a, hh))) .map(MurmurHash3.finalizeHash(_, arity)) - trait Coproduct[F[x] <: Hash[x], A](using inst: CoproductInstances[F, A]) extends DerivedEq.Coproduct[F, A], Safe[A]: + trait SafeCoproduct[F[x] <: Hash[x], A](using inst: CoproductInstances[F, A]) + extends DerivedEq.SafeCoproduct[F, A], + Safe[A]: private[derived] final override def safeHash(x: A): Eval[Int] = Eval.defer(inst.fold[Eval[Int]](x)([t] => (h: F[t], xt: t) => DerivedHash.safeHash(h)(xt))) - - object Strict: - export DerivedHash.coproduct - given product[A <: scala.Product: ProductInstancesOf[Hash]]: DerivedHash[A] = - new Product[Hash, A] {} diff --git a/core/src/main/scala-3/cats/derived/DerivedInvariant.scala b/core/src/main/scala-3/cats/derived/DerivedInvariant.scala index fe326afc..857b5fe1 100644 --- a/core/src/main/scala-3/cats/derived/DerivedInvariant.scala +++ b/core/src/main/scala-3/cats/derived/DerivedInvariant.scala @@ -24,6 +24,13 @@ object DerivedInvariant: import Strict.given summonInline[DerivedInvariant[F]].instance + /** Stack-safe (trampolined via [[cats.Eval]]) derivation. Opt-in: slower on shallow data, but does not overflow the + * stack on deeply nested recursive ADTs. + */ + inline def stackSafe[F[_]]: Invariant[F] = + import StackSafe.given + summonInline[DerivedInvariant[F]].instance + given [T]: DerivedInvariant[Const[T]] = new Invariant[Const[T]]: def imap[A, B](fa: T)(f: A => B)(g: B => A): T = fa @@ -44,6 +51,31 @@ object DerivedInvariant: protected given [F[_]: Invariant |: Derived, G[_]: Invariant |: Derived]: DerivedInvariant[[x] =>> F[G[x]]] = nested + // ---- Default: fast direct recursion ---- + + private def generic[F[_]: InstancesOf[Invariant]]: DerivedInvariant[F] = + new Generic[Invariant, F] {} + + trait Generic[T[f[_]] <: Invariant[f], F[_]](using inst: Instances[T, F]) extends Invariant[F]: + final override def imap[A, B](fa: F[A])(f: A => B)(g: B => A): F[B] = + inst.map(fa)([f[_]] => (F: T[f], fa: f[A]) => F.imap(fa)(f)(g)) + + object Strict: + given product[F[_]: ProductInstancesOf[Invariant]]: DerivedInvariant[F] = generic + given coproduct[F[_]](using inst: => CoproductInstances[Invariant |: Derived, F]): DerivedInvariant[F] = + generic(using inst.unify) + + // ---- Opt-in: stack-safe recursion via Eval ---- + + object StackSafe: + given product[F[_]](using inst: ProductInstances[Invariant |: Derived, F]): DerivedInvariant[F] = + given ProductInstances[Invariant, F] = inst.unify + new SafeProduct[Invariant, F] {} + + given coproduct[F[_]](using inst: => CoproductInstances[Invariant |: Derived, F]): DerivedInvariant[F] = + given CoproductInstances[Invariant, F] = inst.unify + new SafeCoproduct[Invariant, F] {} + private[derived] trait Safe[F[_]] extends Invariant[F]: private[derived] def safeImap[A, B](fa: F[A])(f: A => B)(g: B => A): Eval[F[B]] override def imap[A, B](fa: F[A])(f: A => B)(g: B => A): F[B] = safeImap(fa)(f)(g).value @@ -53,11 +85,7 @@ object DerivedInvariant: case safe: Safe[F] @scala.unchecked => safe.safeImap(fa)(f)(g) case _ => Eval.later(F.imap(fa)(f)(g)) - trait Generic[T[f[_]] <: Invariant[f], F[_]](using inst: Instances[T, F]) extends Invariant[F]: - final override def imap[A, B](fa: F[A])(f: A => B)(g: B => A): F[B] = - inst.map(fa)([f[_]] => (F: T[f], fa: f[A]) => F.imap(fa)(f)(g)) - - trait Product[T[f[_]] <: Invariant[f], F[_]](using inst: ProductInstances[T, F]) extends Safe[F]: + trait SafeProduct[T[f[_]] <: Invariant[f], F[_]](using inst: ProductInstances[T, F]) extends Safe[F]: private[derived] final override def safeImap[A, B](fa: F[A])(f: A => B)(g: B => A): Eval[F[B]] = val pure = [a] => (x: a) => Eval.now(x) val mp = [a, b] => (ea: Eval[a], h: a => b) => ea.map(h) @@ -66,17 +94,9 @@ object DerivedInvariant: [f[_]] => (F: T[f], fa: f[A]) => DerivedInvariant.safeImap(F)(fa)(f)(g) ) - trait Coproduct[T[f[_]] <: Invariant[f], F[_]](using inst: CoproductInstances[T, F]) extends Safe[F]: + trait SafeCoproduct[T[f[_]] <: Invariant[f], F[_]](using inst: CoproductInstances[T, F]) extends Safe[F]: private[derived] final override def safeImap[A, B](fa: F[A])(f: A => B)(g: B => A): Eval[F[B]] = Eval.defer(inst.fold(fa)( [f[a] <: F[a]] => (F: T[f], fa: f[A]) => DerivedInvariant.safeImap(F)(fa)(f)(g).asInstanceOf[Eval[F[B]]] )) - - object Strict: - given product[F[_]: ProductInstancesOf[Invariant]]: DerivedInvariant[F] = - new Product[Invariant, F] {} - - given coproduct[F[_]](using inst: => CoproductInstances[Invariant |: Derived, F]): DerivedInvariant[F] = - given CoproductInstances[Invariant, F] = inst.unify - new Coproduct[Invariant, F] {} diff --git a/core/src/main/scala-3/cats/derived/DerivedNonEmptyTraverse.scala b/core/src/main/scala-3/cats/derived/DerivedNonEmptyTraverse.scala index 4e6c9176..c7781ff3 100644 --- a/core/src/main/scala-3/cats/derived/DerivedNonEmptyTraverse.scala +++ b/core/src/main/scala-3/cats/derived/DerivedNonEmptyTraverse.scala @@ -25,6 +25,14 @@ object DerivedNonEmptyTraverse: import DerivedNonEmptyTraverse.Strict.given summonInline[DerivedNonEmptyTraverse[F]].instance + /** Stack-safe (trampolined via [[cats.Eval]]) derivation. Opt-in: slower on shallow data, but does not overflow the + * stack on deeply nested recursive ADTs. + */ + inline def stackSafe[F[_]]: NonEmptyTraverse[F] = + import DerivedTraverse.StackSafe.given + import DerivedNonEmptyTraverse.StackSafe.given + summonInline[DerivedNonEmptyTraverse[F]].instance + given nested[F[_], G[_]](using F: => (NonEmptyTraverse |: Derived)[F], G: => (NonEmptyTraverse |: Derived)[G] @@ -47,6 +55,22 @@ object DerivedNonEmptyTraverse: protected given [F[_]: NonEmptyTraverse |: Derived, G[_]: NonEmptyTraverse |: Derived] : DerivedNonEmptyTraverse[[x] =>> F[G[x]]] = nested + private type Alt[F[_]] = [A] =>> Either[F[A], A] + private def altApplicative[F[_]](using F: Apply[F]): Applicative[Alt[F]] = new Applicative[Alt[F]]: + override def pure[A](x: A) = Right(x) + override def map[A, B](fa: Alt[F][A])(f: A => B) = fa match + case Left(fa) => Left(F.map(fa)(f)) + case Right(a) => Right(f(a)) + override def ap[A, B](ff: Alt[F][A => B])(fa: Alt[F][A]) = (ff, fa) match + case (Left(ff), Left(fa)) => Left(F.ap(ff)(fa)) + case (Left(ff), Right(a)) => Left(F.map(ff)(_(a))) + case (Right(f), Left(fa)) => Left(F.map(fa)(f)) + case (Right(f), Right(a)) => Right(f(a)) + + private given [F[_]](using F: Apply[F]): Applicative[Alt[F]] = altApplicative[F] + + // ---- Default: fast direct recursion ---- + trait Product[T[x[_]] <: Traverse[x], F[_]](@unused ev: NonEmptyTraverse[?])(using @unused inst: ProductInstances[T, F] ) extends NonEmptyTraverse[F], @@ -54,8 +78,7 @@ object DerivedNonEmptyTraverse: DerivedTraverse.Product[T, F]: final override def nonEmptyTraverse[G[_]: Apply, A, B](fa: F[A])(f: A => G[B]): G[F[B]] = - given Applicative[Alt[G]] = altApplicative[G] - DerivedTraverse.safeTraverse[F, Alt[G], A, B](this)(fa)(f.andThen(Left.apply)).value match + traverse[Alt[G], A, B](fa)(f.andThen(Left.apply)) match case Left(value) => value case Right(_) => ??? @@ -65,33 +88,29 @@ object DerivedNonEmptyTraverse: DerivedTraverse.Coproduct[T, F]: final override def nonEmptyTraverse[G[_], A, B](fa: F[A])(f: A => G[B])(using G: Apply[G]): G[F[B]] = - safeNonEmptyTraverse(fa)(f).value + inst.fold(fa)([f[a] <: F[a]] => (F: T[f], fa: f[A]) => G.widen[f[B], F[B]](F.nonEmptyTraverse(fa)(f))) - private[derived] def safeNonEmptyTraverse[G[_], A, B]( - fa: F[A] - )(f: A => G[B])(using G: Apply[G]): Eval[G[F[B]]] = - Eval.defer(inst.fold(fa): - [f[a] <: F[a]] => (F: T[f], fa: f[A]) => - Eval.later(F.nonEmptyTraverse(fa)(f)).map(g => G.widen[f[B], F[B]](g)).asInstanceOf[Eval[G[F[B]]]] - ) + object Strict: + def product[F[_]: ProductInstancesOf[Traverse]](ev: NonEmptyTraverse[?]): DerivedNonEmptyTraverse[F] = + new Product[Traverse, F](ev) + with DerivedReducible.Product[Traverse, F](ev) + with DerivedTraverse.Product[Traverse, F] + with DerivedFunctor.Generic[Traverse, F] {} - private type Alt[F[_]] = [A] =>> Either[F[A], A] - private def altApplicative[F[_]](using F: Apply[F]): Applicative[Alt[F]] = new Applicative[Alt[F]]: - override def pure[A](x: A) = Right(x) - override def map[A, B](fa: Alt[F][A])(f: A => B) = fa match - case Left(fa) => Left(F.map(fa)(f)) - case Right(a) => Right(f(a)) - override def ap[A, B](ff: Alt[F][A => B])(fa: Alt[F][A]) = (ff, fa) match - case (Left(ff), Left(fa)) => Left(F.ap(ff)(fa)) - case (Left(ff), Right(a)) => Left(F.map(ff)(_(a))) - case (Right(f), Left(fa)) => Left(F.map(fa)(f)) - case (Right(f), Right(a)) => Right(f(a)) + inline given product[F[_]](using gen: ProductGeneric[F]): DerivedNonEmptyTraverse[F] = + product(summonFirst[NonEmptyTraverse, gen.MirroredElemTypes]) - private given [F[_]](using F: Apply[F]): Applicative[Alt[F]] = altApplicative[F] + given coproduct[F[_]](using + inst: => CoproductInstances[NonEmptyTraverse |: Derived, F] + ): DerivedNonEmptyTraverse[F] = + given CoproductInstances[NonEmptyTraverse, F] = inst.unify + new NonEmptyTraverse[F] with Coproduct[NonEmptyTraverse, F] {} - object Strict: + // ---- Opt-in: stack-safe recursion via Eval ---- + + object StackSafe: def product[F[_]: ProductInstancesOf[Traverse]](ev: NonEmptyTraverse[?]): DerivedNonEmptyTraverse[F] = - new Product[Traverse, F](ev) with DerivedReducible.Product[Traverse, F](ev) {} + new SafeProduct[Traverse, F](ev) with DerivedReducible.SafeProduct[Traverse, F](ev) {} inline given product[F[_]](using gen: ProductGeneric[F]): DerivedNonEmptyTraverse[F] = product(summonFirst[NonEmptyTraverse, gen.MirroredElemTypes]) @@ -100,4 +119,32 @@ object DerivedNonEmptyTraverse: inst: => CoproductInstances[NonEmptyTraverse |: Derived, F] ): DerivedNonEmptyTraverse[F] = given CoproductInstances[NonEmptyTraverse, F] = inst.unify - new Coproduct[NonEmptyTraverse, F] {} + new SafeCoproduct[NonEmptyTraverse, F] {} + + trait SafeProduct[T[x[_]] <: Traverse[x], F[_]](@unused ev: NonEmptyTraverse[?])(using + @unused inst: ProductInstances[T, F] + ) extends NonEmptyTraverse[F], + DerivedReducible.SafeProduct[T, F], + DerivedTraverse.SafeProduct[T, F]: + + final override def nonEmptyTraverse[G[_]: Apply, A, B](fa: F[A])(f: A => G[B]): G[F[B]] = + given Applicative[Alt[G]] = altApplicative[G] + DerivedTraverse.safeTraverse[F, Alt[G], A, B](this)(fa)(f.andThen(Left.apply)).value match + case Left(value) => value + case Right(_) => ??? + + trait SafeCoproduct[T[x[_]] <: NonEmptyTraverse[x], F[_]](using inst: CoproductInstances[T, F]) + extends NonEmptyTraverse[F], + DerivedReducible.SafeCoproduct[T, F], + DerivedTraverse.SafeCoproduct[T, F]: + + final override def nonEmptyTraverse[G[_], A, B](fa: F[A])(f: A => G[B])(using G: Apply[G]): G[F[B]] = + safeNonEmptyTraverse(fa)(f).value + + private[derived] def safeNonEmptyTraverse[G[_], A, B]( + fa: F[A] + )(f: A => G[B])(using G: Apply[G]): Eval[G[F[B]]] = + Eval.defer(inst.fold(fa): + [f[a] <: F[a]] => (F: T[f], fa: f[A]) => + Eval.later(F.nonEmptyTraverse(fa)(f)).map(g => G.widen[f[B], F[B]](g)).asInstanceOf[Eval[G[F[B]]]] + ) diff --git a/core/src/main/scala-3/cats/derived/DerivedOrder.scala b/core/src/main/scala-3/cats/derived/DerivedOrder.scala index 2569410c..f3d00787 100644 --- a/core/src/main/scala-3/cats/derived/DerivedOrder.scala +++ b/core/src/main/scala-3/cats/derived/DerivedOrder.scala @@ -22,6 +22,13 @@ object DerivedOrder: import Strict.given summonInline[DerivedOrder[A]].instance + /** Stack-safe (trampolined via [[cats.Eval]]) derivation. Opt-in: slower on shallow data, but does not overflow the + * stack on deeply nested recursive ADTs. + */ + inline def stackSafe[A]: Order[A] = + import StackSafe.given + summonInline[DerivedOrder[A]].instance + @unused given singleton[A <: Singleton: ValueOf]: DerivedOrder[A] = Order.allEqual @@ -33,6 +40,38 @@ object DerivedOrder: given CoproductInstances[Order, A] = inst.unify new Coproduct[Order, A] {} + // ---- Default: fast direct recursion ---- + + trait Product[T[x] <: Order[x], A](using inst: ProductInstances[T, A]) extends Order[A]: + def compare(x: A, y: A): Int = + inst.foldLeft2(x, y)(0: Int): + [t] => + (acc: Int, ord: T[t], t0: t, t1: t) => + val cmp = ord.compare(t0, t1) + Complete(cmp != 0)(cmp)(acc) + + trait Coproduct[T[x] <: Order[x], A](using inst: CoproductInstances[T, A]) extends Order[A]: + def compare(x: A, y: A): Int = + inst.fold2(x, y)((x: Int, y: Int) => x - y): + [t] => (ord: T[t], t0: t, t1: t) => ord.compare(t0, t1) + + object Strict: + export DerivedOrder.coproduct + given product[A: ProductInstancesOf[Order]]: DerivedOrder[A] = + new Product[Order, A] {} + + // ---- Opt-in: stack-safe recursion via Eval ---- + + object StackSafe: + export DerivedOrder.singleton + given product[A](using inst: => ProductInstances[Order |: Derived, A]): DerivedOrder[A] = + given ProductInstances[Order, A] = inst.unify + new SafeProduct[Order, A] {} + + given coproduct[A](using inst: => CoproductInstances[Order |: Derived, A]): DerivedOrder[A] = + given CoproductInstances[Order, A] = inst.unify + new SafeCoproduct[Order, A] {} + private[derived] trait Safe[A] extends Order[A]: private[derived] def safeCompare(x: A, y: A): Eval[Int] override def compare(x: A, y: A): Int = safeCompare(x, y).value @@ -42,7 +81,7 @@ object DerivedOrder: case safe: Safe[?] => safe.asInstanceOf[Safe[A]].safeCompare(x, y) case _ => Eval.later(F.compare(x, y)) - trait Product[T[x] <: Order[x], A](using inst: ProductInstances[T, A]) extends Safe[A]: + trait SafeProduct[T[x] <: Order[x], A](using inst: ProductInstances[T, A]) extends Safe[A]: private[derived] final override def safeCompare(x: A, y: A): Eval[Int] = inst.foldLeft2[Eval[Int]](x, y)(Eval.now(0)): [t] => (acc: Eval[Int], ord: T[t], t0: t, t1: t) => @@ -50,13 +89,8 @@ object DerivedOrder: if cmp != 0 then Eval.now(cmp) else DerivedOrder.safeCompare(ord)(t0, t1) Complete(false)(next)(next) - trait Coproduct[T[x] <: Order[x], A](using inst: CoproductInstances[T, A]) extends Safe[A]: + trait SafeCoproduct[T[x] <: Order[x], A](using inst: CoproductInstances[T, A]) extends Safe[A]: private[derived] final override def safeCompare(x: A, y: A): Eval[Int] = Eval.defer(inst.fold2(x, y)((x: Int, y: Int) => Eval.now(x - y)): [t] => (ord: T[t], t0: t, t1: t) => DerivedOrder.safeCompare(ord)(t0, t1) ) - - object Strict: - export DerivedOrder.coproduct - given product[A: ProductInstancesOf[Order]]: DerivedOrder[A] = - new Product[Order, A] {} diff --git a/core/src/main/scala-3/cats/derived/DerivedPartialOrder.scala b/core/src/main/scala-3/cats/derived/DerivedPartialOrder.scala index dd476ca2..ffa38f85 100644 --- a/core/src/main/scala-3/cats/derived/DerivedPartialOrder.scala +++ b/core/src/main/scala-3/cats/derived/DerivedPartialOrder.scala @@ -22,6 +22,13 @@ object DerivedPartialOrder: import Strict.given summonInline[DerivedPartialOrder[A]].instance + /** Stack-safe (trampolined via [[cats.Eval]]) derivation. Opt-in: slower on shallow data, but does not overflow the + * stack on deeply nested recursive ADTs. + */ + inline def stackSafe[A]: PartialOrder[A] = + import StackSafe.given + summonInline[DerivedPartialOrder[A]].instance + @unused given singleton[A <: Singleton: ValueOf]: DerivedPartialOrder[A] = Order.allEqual @@ -33,6 +40,38 @@ object DerivedPartialOrder: given CoproductInstances[PartialOrder, A] = inst.unify new Coproduct[PartialOrder, A] {} + // ---- Default: fast direct recursion ---- + + trait Product[T[x] <: PartialOrder[x], A](using inst: ProductInstances[T, A]) extends PartialOrder[A]: + def partialCompare(x: A, y: A): Double = + inst.foldLeft2(x, y)(0: Double): + [t] => + (acc: Double, ord: T[t], t0: t, t1: t) => + val cmp = ord.partialCompare(t0, t1) + Complete(cmp != 0)(cmp)(acc) + + trait Coproduct[T[x] <: PartialOrder[x], A](using inst: CoproductInstances[T, A]) extends PartialOrder[A]: + def partialCompare(x: A, y: A): Double = + inst.fold2(x, y)(Double.NaN: Double): + [t] => (ord: T[t], t0: t, t1: t) => ord.partialCompare(t0, t1) + + object Strict: + export DerivedPartialOrder.coproduct + given product[A: ProductInstancesOf[PartialOrder]]: DerivedPartialOrder[A] = + new Product[PartialOrder, A] {} + + // ---- Opt-in: stack-safe recursion via Eval ---- + + object StackSafe: + export DerivedPartialOrder.singleton + given product[A](using inst: => ProductInstances[PartialOrder |: Derived, A]): DerivedPartialOrder[A] = + given ProductInstances[PartialOrder, A] = inst.unify + new SafeProduct[PartialOrder, A] {} + + given coproduct[A](using inst: => CoproductInstances[PartialOrder |: Derived, A]): DerivedPartialOrder[A] = + given CoproductInstances[PartialOrder, A] = inst.unify + new SafeCoproduct[PartialOrder, A] {} + private[derived] trait Safe[A] extends PartialOrder[A]: private[derived] def safePartialCompare(x: A, y: A): Eval[Double] override def partialCompare(x: A, y: A): Double = safePartialCompare(x, y).value @@ -42,7 +81,7 @@ object DerivedPartialOrder: case safe: Safe[?] => safe.asInstanceOf[Safe[A]].safePartialCompare(x, y) case _ => Eval.later(F.partialCompare(x, y)) - trait Product[T[x] <: PartialOrder[x], A](using inst: ProductInstances[T, A]) extends Safe[A]: + trait SafeProduct[T[x] <: PartialOrder[x], A](using inst: ProductInstances[T, A]) extends Safe[A]: private[derived] final override def safePartialCompare(x: A, y: A): Eval[Double] = inst.foldLeft2[Eval[Double]](x, y)(Eval.now(0.0)): [t] => (acc: Eval[Double], ord: T[t], t0: t, t1: t) => @@ -50,13 +89,8 @@ object DerivedPartialOrder: if cmp != 0.0 then Eval.now(cmp) else DerivedPartialOrder.safePartialCompare(ord)(t0, t1) Complete(false)(next)(next) - trait Coproduct[T[x] <: PartialOrder[x], A](using inst: CoproductInstances[T, A]) extends Safe[A]: + trait SafeCoproduct[T[x] <: PartialOrder[x], A](using inst: CoproductInstances[T, A]) extends Safe[A]: private[derived] final override def safePartialCompare(x: A, y: A): Eval[Double] = Eval.defer(inst.fold2(x, y)(Eval.now(Double.NaN): Eval[Double]): [t] => (ord: T[t], t0: t, t1: t) => DerivedPartialOrder.safePartialCompare(ord)(t0, t1) ) - - object Strict: - export DerivedPartialOrder.coproduct - given product[A: ProductInstancesOf[PartialOrder]]: DerivedPartialOrder[A] = - new Product[PartialOrder, A] {} diff --git a/core/src/main/scala-3/cats/derived/DerivedReducible.scala b/core/src/main/scala-3/cats/derived/DerivedReducible.scala index f51d11ac..b4ba2ac0 100644 --- a/core/src/main/scala-3/cats/derived/DerivedReducible.scala +++ b/core/src/main/scala-3/cats/derived/DerivedReducible.scala @@ -25,6 +25,14 @@ object DerivedReducible: import DerivedReducible.Strict.given summonInline[DerivedReducible[F]].instance + /** Stack-safe (trampolined via [[cats.Eval]]) derivation. Opt-in: slower on shallow data, but does not overflow the + * stack on deeply nested recursive ADTs. + */ + inline def stackSafe[F[_]]: Reducible[F] = + import DerivedFoldable.StackSafe.given + import DerivedReducible.StackSafe.given + summonInline[DerivedReducible[F]].instance + given nested[F[_], G[_]](using F: => (Reducible |: Derived)[F], G: => (Reducible |: Derived)[G] @@ -45,6 +53,68 @@ object DerivedReducible: protected given [F[_]: Reducible |: Derived, G[_]: Reducible |: Derived]: DerivedReducible[[x] =>> F[G[x]]] = nested + // ---- Default: fast direct recursion ---- + + trait Product[T[f[_]] <: Foldable[f], F[_]](@unused ev: Reducible[?])(using inst: ProductInstances[T, F]) + extends DerivedFoldable.Product[T, F], + Reducible[F]: + + private val evalNone = Eval.now(None) + + final override def reduceLeftTo[A, B](fa: F[A])(f: A => B)(g: (B, A) => B): B = + inst + .foldLeft[A, Option[B]](fa)(None): + [f[_]] => + (acc: Option[B], F: T[f], fa: f[A]) => + acc match + case Some(b) => Some(F.foldLeft(fa, b)(g)) + case None => F.reduceLeftToOption(fa)(f)(g) + .get + + final override def reduceRightTo[A, B](fa: F[A])(f: A => B)(g: (A, Eval[B]) => Eval[B]): Eval[B] = + inst + .foldRight[A, Eval[Option[B]]](fa)(evalNone): + [f[_]] => + (F: T[f], fa: f[A], acc: Eval[Option[B]]) => + acc.flatMap: + case Some(b) => F.foldRight(fa, Eval.now(b))(g).map(Some.apply) + case None => F.reduceRightToOption(fa)(f)(g) + .map(_.get) + + trait Coproduct[T[f[_]] <: Reducible[f], F[_]](using inst: CoproductInstances[T, F]) + extends DerivedFoldable.Coproduct[T, F], + Reducible[F]: + + final override def reduceLeftTo[A, B](fa: F[A])(f: A => B)(g: (B, A) => B): B = + inst.fold(fa)([f[_]] => (F: T[f], fa: f[A]) => F.reduceLeftTo(fa)(f)(g)) + + final override def reduceRightTo[A, B](fa: F[A])(f: A => B)(g: (A, Eval[B]) => Eval[B]): Eval[B] = + inst.fold(fa)([f[_]] => (F: T[f], fa: f[A]) => Eval.defer(F.reduceRightTo(fa)(f)(g))) + + object Strict: + def product[F[_]: ProductInstancesOf[Foldable]](ev: Reducible[?]): DerivedReducible[F] = + new Product[Foldable, F](ev) {} + + inline given product[F[_]](using gen: ProductGeneric[F]): DerivedReducible[F] = + product(summonFirst[Reducible, gen.MirroredElemTypes]) + + given coproduct[F[_]](using inst: => CoproductInstances[Reducible |: Derived, F]): DerivedReducible[F] = + given CoproductInstances[Reducible, F] = inst.unify + new Coproduct[Reducible, F] {} + + // ---- Opt-in: stack-safe recursion via Eval ---- + + object StackSafe: + def product[F[_]: ProductInstancesOf[Foldable]](ev: Reducible[?]): DerivedReducible[F] = + new SafeProduct[Foldable, F](ev) {} + + inline given product[F[_]](using gen: ProductGeneric[F]): DerivedReducible[F] = + product(summonFirst[Reducible, gen.MirroredElemTypes]) + + given coproduct[F[_]](using inst: => CoproductInstances[Reducible |: Derived, F]): DerivedReducible[F] = + given CoproductInstances[Reducible, F] = inst.unify + new SafeCoproduct[Reducible, F] {} + private[derived] trait Safe[F[_]] extends Reducible[F]: private[derived] def safeReduceLeftTo[A, B](fa: F[A])(f: A => B)(g: (B, A) => B): Eval[B] override def reduceLeftTo[A, B](fa: F[A])(f: A => B)(g: (B, A) => B): B = @@ -57,9 +127,9 @@ object DerivedReducible: case safe: Safe[F] @scala.unchecked => safe.safeReduceLeftTo(fa)(f)(g) case _ => Eval.later(F.reduceLeftTo(fa)(f)(g)) - trait Product[T[f[_]] <: Foldable[f], F[_]](@unused ev: Reducible[?])(using inst: ProductInstances[T, F]) + trait SafeProduct[T[f[_]] <: Foldable[f], F[_]](@unused ev: Reducible[?])(using inst: ProductInstances[T, F]) extends Safe[F], - DerivedFoldable.Product[T, F]: + DerivedFoldable.SafeProduct[T, F]: private val evalNone = Eval.now(None) @@ -85,9 +155,9 @@ object DerivedReducible: case None => F.reduceRightToOption(fa)(f)(g) .map(_.get) - trait Coproduct[T[f[_]] <: Reducible[f], F[_]](using inst: CoproductInstances[T, F]) + trait SafeCoproduct[T[f[_]] <: Reducible[f], F[_]](using inst: CoproductInstances[T, F]) extends Safe[F], - DerivedFoldable.Coproduct[T, F]: + DerivedFoldable.SafeCoproduct[T, F]: private[derived] final override def safeReduceLeftTo[A, B](fa: F[A])(f: A => B)(g: (B, A) => B): Eval[B] = Eval.defer(inst.fold(fa)([f[_]] => (F: T[f], fa: f[A]) => @@ -96,14 +166,3 @@ object DerivedReducible: final override def reduceRightTo[A, B](fa: F[A])(f: A => B)(g: (A, Eval[B]) => Eval[B]): Eval[B] = inst.fold(fa)([f[_]] => (F: T[f], fa: f[A]) => Eval.defer(F.reduceRightTo(fa)(f)(g))) - - object Strict: - def product[F[_]: ProductInstancesOf[Foldable]](ev: Reducible[?]): DerivedReducible[F] = - new Product[Foldable, F](ev) {} - - inline given product[F[_]](using gen: ProductGeneric[F]): DerivedReducible[F] = - product(summonFirst[Reducible, gen.MirroredElemTypes]) - - given coproduct[F[_]](using inst: => CoproductInstances[Reducible |: Derived, F]): DerivedReducible[F] = - given CoproductInstances[Reducible, F] = inst.unify - new Coproduct[Reducible, F] {} diff --git a/core/src/main/scala-3/cats/derived/DerivedShow.scala b/core/src/main/scala-3/cats/derived/DerivedShow.scala index 66e5b6b1..6bc3330c 100644 --- a/core/src/main/scala-3/cats/derived/DerivedShow.scala +++ b/core/src/main/scala-3/cats/derived/DerivedShow.scala @@ -22,6 +22,14 @@ object DerivedShow: import Strict.given summonInline[DerivedShow[A]].instance + /** Stack-safe (trampolined via [[cats.Eval]]) derivation. Opt-in: slower on shallow data, but does not overflow the + * stack on deeply nested recursive ADTs. + */ + inline def stackSafe[A]: Show[A] = + import StackSafe.given + summonInline[DerivedShow[A]].instance + + // These instances support singleton types unlike the instances in Cats' core. given boolean[A <: Boolean]: DerivedShow[A] = Show.fromToString given byte[A <: Byte]: DerivedShow[A] = Show.fromToString given short[A <: Short]: DerivedShow[A] = Show.fromToString @@ -40,6 +48,52 @@ object DerivedShow: given [A](using => CoproductInstances[Show |: Derived, A]): DerivedShow[A] = Strict.coproduct + // ---- Default: fast direct recursion ---- + + trait Product[F[x] <: Show[x], A](using inst: ProductInstances[F, A], labelling: Labelling[A]) extends Show[A]: + def show(a: A): String = + val prefix = labelling.label + val labels = labelling.elemLabels + val n = labels.size + if n <= 0 then prefix + else + val sb = new StringBuilder(prefix) + sb.append('(') + var i = 0 + while i < n do + sb.append(labels(i)) + sb.append(" = ") + sb.append(inst.project(a)(i)([t] => (show: F[t], x: t) => show.show(x))) + sb.append(", ") + i += 1 + + val l = sb.length + sb.delete(l - 2, l) + sb.append(')') + sb.toString + + trait Coproduct[F[x] <: Show[x], A](using inst: CoproductInstances[F, A]) extends Show[A]: + def show(a: A): String = inst.fold(a)([t] => (st: F[t], t: t) => st.show(t)) + + object Strict: + given product[A: Labelling](using => ProductInstances[Show, A]): DerivedShow[A] = + new Product[Show, A] {} + + given coproduct[A](using inst: => CoproductInstances[Show |: Derived, A]): DerivedShow[A] = + given CoproductInstances[Show, A] = inst.unify + new Coproduct[Show, A] {} + + // ---- Opt-in: stack-safe recursion via Eval ---- + + object StackSafe: + given product[A](using inst: ProductInstances[Show |: Derived, A], labelling: Labelling[A]): DerivedShow[A] = + given ProductInstances[Show, A] = inst.unify + new SafeProduct[Show, A] {} + + given coproduct[A](using inst: => CoproductInstances[Show |: Derived, A]): DerivedShow[A] = + given CoproductInstances[Show, A] = inst.unify + new SafeCoproduct[Show, A] {} + private[derived] trait Safe[A] extends Show[A]: private[derived] def safeShow(a: A): Eval[String] override def show(a: A): String = safeShow(a).value @@ -49,7 +103,7 @@ object DerivedShow: case safe: Safe[?] => safe.asInstanceOf[Safe[A]].safeShow(a) case _ => Eval.later(F.show(a)) - trait Product[F[x] <: Show[x], A](using inst: ProductInstances[F, A], labelling: Labelling[A]) extends Safe[A]: + trait SafeProduct[F[x] <: Show[x], A](using inst: ProductInstances[F, A], labelling: Labelling[A]) extends Safe[A]: private[derived] final override def safeShow(a: A): Eval[String] = val prefix = labelling.label val labels = labelling.elemLabels @@ -71,14 +125,6 @@ object DerivedShow: built.append(')') built.toString - trait Coproduct[F[x] <: Show[x], A](using inst: CoproductInstances[F, A]) extends Safe[A]: + trait SafeCoproduct[F[x] <: Show[x], A](using inst: CoproductInstances[F, A]) extends Safe[A]: private[derived] final override def safeShow(a: A): Eval[String] = Eval.defer(inst.fold[Eval[String]](a)([t] => (st: F[t], t: t) => DerivedShow.safeShow(st)(t))) - - object Strict: - given product[A: Labelling](using => ProductInstances[Show, A]): DerivedShow[A] = - new Product[Show, A] {} - - given coproduct[A](using inst: => CoproductInstances[Show |: Derived, A]): DerivedShow[A] = - given CoproductInstances[Show, A] = inst.unify - new Coproduct[Show, A] {} diff --git a/core/src/main/scala-3/cats/derived/DerivedTraverse.scala b/core/src/main/scala-3/cats/derived/DerivedTraverse.scala index 35db56ef..af530ef7 100644 --- a/core/src/main/scala-3/cats/derived/DerivedTraverse.scala +++ b/core/src/main/scala-3/cats/derived/DerivedTraverse.scala @@ -24,6 +24,13 @@ object DerivedTraverse: import Strict.given summonInline[DerivedTraverse[F]].instance + /** Stack-safe (trampolined via [[cats.Eval]]) derivation. Opt-in: slower on shallow data, but does not overflow the + * stack on deeply nested recursive ADTs. + */ + inline def stackSafe[F[_]]: Traverse[F] = + import StackSafe.given + summonInline[DerivedTraverse[F]].instance + given [T]: DerivedTraverse[Const[T]] = new Traverse[Const[T]]: override def map[A, B](fa: T)(f: A => B): T = fa override def foldLeft[A, B](fa: T, b: B)(f: (B, A) => B): B = b @@ -46,6 +53,46 @@ object DerivedTraverse: @deprecated("Kept for binary compatibility", "3.2.0") protected given [F[_]: Traverse |: Derived, G[_]: Traverse |: Derived]: DerivedTraverse[[x] =>> F[G[x]]] = nested + // ---- Default: fast direct recursion ---- + + trait Product[T[f[_]] <: Traverse[f], F[_]](using inst: ProductInstances[T, F]) + extends Traverse[F], + DerivedFunctor.Generic[T, F], + DerivedFoldable.Product[T, F]: + + final override def traverse[G[_], A, B](fa: F[A])(f: A => G[B])(using G: Applicative[G]): G[F[B]] = + val pure = [a] => (x: a) => G.pure(x) + val map = [a, b] => (ga: G[a], f: a => b) => G.map(ga)(f) + val ap = [a, b] => (gf: G[a => b], ga: G[a]) => G.ap(gf)(ga) + inst.traverse[A, G, B](fa)(map)(pure)(ap)([f[_]] => (F: T[f], fa: f[A]) => F.traverse(fa)(f)) + + trait Coproduct[T[f[_]] <: Traverse[f], F[_]](using inst: CoproductInstances[T, F]) + extends Traverse[F], + DerivedFunctor.Generic[T, F], + DerivedFoldable.Coproduct[T, F]: + + final override def traverse[G[_], A, B](fa: F[A])(f: A => G[B])(using G: Applicative[G]): G[F[B]] = + inst.fold(fa)([f[a] <: F[a]] => (F: T[f], fa: f[A]) => G.widen[f[B], F[B]](F.traverse(fa)(f))) + + object Strict: + given product[F[_]: ProductInstancesOf[Traverse]]: DerivedTraverse[F] = + new Traverse[F] with Product[Traverse, F] {} + + given coproduct[F[_]](using inst: => CoproductInstances[Traverse |: Derived, F]): DerivedTraverse[F] = + given CoproductInstances[Traverse, F] = inst.unify + new Traverse[F] with Coproduct[Traverse, F] {} + + // ---- Opt-in: stack-safe recursion via Eval ---- + + object StackSafe: + given product[F[_]](using inst: ProductInstances[Traverse |: Derived, F]): DerivedTraverse[F] = + given ProductInstances[Traverse, F] = inst.unify + new Traverse[F] with SafeProduct[Traverse, F] {} + + given coproduct[F[_]](using inst: => CoproductInstances[Traverse |: Derived, F]): DerivedTraverse[F] = + given CoproductInstances[Traverse, F] = inst.unify + new Traverse[F] with SafeCoproduct[Traverse, F] {} + private[derived] trait Safe[F[_]] extends Traverse[F]: private[derived] def safeTraverse[G[_], A, B](fa: F[A])(f: A => G[B])(using G: Applicative[G]): Eval[G[F[B]]] override def traverse[G[_], A, B](fa: F[A])(f: A => G[B])(using G: Applicative[G]): G[F[B]] = @@ -58,10 +105,10 @@ object DerivedTraverse: case safe: Safe[F] @scala.unchecked => safe.safeTraverse(fa)(f) case _ => Eval.later(F.traverse(fa)(f)) - trait Product[T[f[_]] <: Traverse[f], F[_]](using inst: ProductInstances[T, F]) + trait SafeProduct[T[f[_]] <: Traverse[f], F[_]](using inst: ProductInstances[T, F]) extends Safe[F], - DerivedFunctor.Product[T, F], - DerivedFoldable.Product[T, F]: + DerivedFunctor.SafeProduct[T, F], + DerivedFoldable.SafeProduct[T, F]: private[derived] final override def safeTraverse[G[_], A, B]( fa: F[A] @@ -74,10 +121,10 @@ object DerivedTraverse: [f[_]] => (F: T[f], fa: f[A]) => DerivedTraverse.safeTraverse(F)(fa)(f) ) - trait Coproduct[T[f[_]] <: Traverse[f], F[_]](using inst: CoproductInstances[T, F]) + trait SafeCoproduct[T[f[_]] <: Traverse[f], F[_]](using inst: CoproductInstances[T, F]) extends Safe[F], - DerivedFunctor.Coproduct[T, F], - DerivedFoldable.Coproduct[T, F]: + DerivedFunctor.SafeCoproduct[T, F], + DerivedFoldable.SafeCoproduct[T, F]: private[derived] final override def safeTraverse[G[_], A, B]( fa: F[A] @@ -86,11 +133,3 @@ object DerivedTraverse: [f[a] <: F[a]] => (F: T[f], fa: f[A]) => DerivedTraverse.safeTraverse(F)(fa)(f).map(g => G.widen[f[B], F[B]](g)).asInstanceOf[Eval[G[F[B]]]] ) - - object Strict: - given product[F[_]: ProductInstancesOf[Traverse]]: DerivedTraverse[F] = - new Traverse[F] with Product[Traverse, F] {} - - given coproduct[F[_]](using inst: => CoproductInstances[Traverse |: Derived, F]): DerivedTraverse[F] = - given CoproductInstances[Traverse, F] = inst.unify - new Traverse[F] with Coproduct[Traverse, F] {} diff --git a/core/src/main/scala-3/cats/derived/package.scala b/core/src/main/scala-3/cats/derived/package.scala index b12c2322..01d1a61c 100644 --- a/core/src/main/scala-3/cats/derived/package.scala +++ b/core/src/main/scala-3/cats/derived/package.scala @@ -158,6 +158,37 @@ object strict: inline def bifoldable[F[_, _]]: Bifoldable[F] = DerivedBifoldable.strict[F] inline def bitraverse[F[_, _]]: Bitraverse[F] = DerivedBitraverse.strict[F] +/** Stack-safe (trampolined via [[cats.Eval]]) variants of the derivations. Opt-in: slower on shallow data, but do not + * overflow the stack on deeply nested recursive ADTs. + */ +object stackSafe: + extension (x: Eq.type) inline def derived[A]: Eq[A] = DerivedEq.stackSafe[A] + extension (x: Hash.type) inline def derived[A]: Hash[A] = DerivedHash.stackSafe[A] + extension (x: PartialOrder.type) inline def derived[A]: PartialOrder[A] = DerivedPartialOrder.stackSafe[A] + extension (x: Order.type) inline def derived[A]: Order[A] = DerivedOrder.stackSafe[A] + extension (x: Show.type) inline def derived[A]: Show[A] = DerivedShow.stackSafe[A] + extension (x: Invariant.type) inline def derived[F[_]]: Invariant[F] = DerivedInvariant.stackSafe[F] + extension (x: Functor.type) inline def derived[F[_]]: Functor[F] = DerivedFunctor.stackSafe[F] + extension (x: Contravariant.type) inline def derived[F[_]]: Contravariant[F] = DerivedContravariant.stackSafe[F] + extension (x: Foldable.type) inline def derived[F[_]]: Foldable[F] = DerivedFoldable.stackSafe[F] + extension (x: Reducible.type) inline def derived[F[_]]: Reducible[F] = DerivedReducible.stackSafe[F] + extension (x: Traverse.type) inline def derived[F[_]]: Traverse[F] = DerivedTraverse.stackSafe[F] + extension (x: NonEmptyTraverse.type) inline def derived[F[_]]: NonEmptyTraverse[F] = DerivedNonEmptyTraverse.stackSafe[F] + + object semiauto: + inline def eq[A]: Eq[A] = DerivedEq.stackSafe[A] + inline def hash[A]: Hash[A] = DerivedHash.stackSafe[A] + inline def partialOrder[A]: PartialOrder[A] = DerivedPartialOrder.stackSafe[A] + inline def order[A]: Order[A] = DerivedOrder.stackSafe[A] + inline def show[A]: Show[A] = DerivedShow.stackSafe[A] + inline def invariant[F[_]]: Invariant[F] = DerivedInvariant.stackSafe[F] + inline def functor[F[_]]: Functor[F] = DerivedFunctor.stackSafe[F] + inline def contravariant[F[_]]: Contravariant[F] = DerivedContravariant.stackSafe[F] + inline def foldable[F[_]]: Foldable[F] = DerivedFoldable.stackSafe[F] + inline def reducible[F[_]]: Reducible[F] = DerivedReducible.stackSafe[F] + inline def traverse[F[_]]: Traverse[F] = DerivedTraverse.stackSafe[F] + inline def nonEmptyTraverse[F[_]]: NonEmptyTraverse[F] = DerivedNonEmptyTraverse.stackSafe[F] + object auto: private type NotGiven0[F[_]] = [A] =>> NotGiven[F[A]] private type NotGiven1[T[_[_]]] = [F[_]] =>> NotGiven[T[F]] diff --git a/core/src/test/scala-3/cats/derived/ContravariantSuite.scala b/core/src/test/scala-3/cats/derived/ContravariantSuite.scala index 5fa5ccb6..32b46755 100644 --- a/core/src/test/scala-3/cats/derived/ContravariantSuite.scala +++ b/core/src/test/scala-3/cats/derived/ContravariantSuite.scala @@ -57,14 +57,9 @@ class ContravariantSuite extends KittensSuite: testNoInstance("strict.semiauto.contravariant", "TopK") locally: - import semiInstances.given + given Contravariant[EnumK1Contra] = stackSafe.semiauto.contravariant val Size = 10000 - test("semiauto.contravariant is stack safe for recursive ListPred"): - val longList: ListPred[Int] = List.fill(Size)((_: Int) => true) - val contramapped = Contravariant[ListPred].contramap(longList)((s: String) => s.length) - assertEquals(contramapped.size, Size) - - test("semiauto.contravariant is stack safe for recursive EnumK1Contra"): + test("stackSafe.semiauto.contravariant is stack safe for recursive EnumK1Contra"): val tree = (1 to Size).foldLeft[EnumK1Contra[Int]](EnumK1Contra.Leaf((_: Int) => ())): (acc, _) => EnumK1Contra.Rec(EnumK1Contra.Leaf((_: Int) => ()), acc) val contramapped = Contravariant[EnumK1Contra].contramap(tree)((s: String) => s.length) diff --git a/core/src/test/scala-3/cats/derived/FunctorSuite.scala b/core/src/test/scala-3/cats/derived/FunctorSuite.scala index 286353eb..10ead8ae 100644 --- a/core/src/test/scala-3/cats/derived/FunctorSuite.scala +++ b/core/src/test/scala-3/cats/derived/FunctorSuite.scala @@ -64,14 +64,15 @@ class FunctorSuite extends KittensSuite: testNoInstance("strict.semiauto.functor", "TopK") locally: - import semiInstances.given + given Functor[IList] = stackSafe.semiauto.functor + given Functor[EnumK1] = stackSafe.semiauto.functor val Size = 50000 - test("semiauto.functor is stack safe for recursive IList"): + test("stackSafe.semiauto.functor is stack safe for recursive IList"): val list = (1 to Size).foldLeft[IList[Int]](INil())((acc, i) => ICons(i, acc)) val mapped = Functor[IList].map(list)(_ + 1) assertEquals(IList.toList(mapped).size, Size) - test("semiauto.functor is stack safe for recursive EnumK1"): + test("stackSafe.semiauto.functor is stack safe for recursive EnumK1"): val tree = (1 to Size).foldLeft[EnumK1[Int]](EnumK1.Leaf(0)): (acc, i) => EnumK1.Rec(EnumK1.Leaf(i), acc) val mapped = Functor[EnumK1].map(tree)(_ + 1) diff --git a/core/src/test/scala-3/cats/derived/StackSafetySuite.scala b/core/src/test/scala-3/cats/derived/StackSafetySuite.scala index 1dee2560..996a04b4 100644 --- a/core/src/test/scala-3/cats/derived/StackSafetySuite.scala +++ b/core/src/test/scala-3/cats/derived/StackSafetySuite.scala @@ -1,50 +1,40 @@ package cats.derived import cats.{Eq, Hash, Invariant, Order, PartialOrder, Show} -import cats.{Bifunctor, Bifoldable, Bitraverse} import cats.{Foldable, NonEmptyTraverse, Reducible, Traverse} object StackSafetySuite: import ADTs.* object eqInst: - given Eq[IList[Int]] = semiauto.eq + given Eq[IList[Int]] = stackSafe.semiauto.eq object orderInst: - given Order[IList[Int]] = semiauto.order + given Order[IList[Int]] = stackSafe.semiauto.order object pOrderInst: - given PartialOrder[IList[Int]] = semiauto.partialOrder + given PartialOrder[IList[Int]] = stackSafe.semiauto.partialOrder object hashInst: - given Hash[IList[Int]] = semiauto.hash + given Hash[IList[Int]] = stackSafe.semiauto.hash object showInst: - given Show[IList[Int]] = semiauto.show + given Show[IList[Int]] = stackSafe.semiauto.show object invariantInst: - given Invariant[EnumK1] = semiauto.invariant + given Invariant[EnumK1] = stackSafe.semiauto.invariant object foldableInst: - given Foldable[EnumK1] = semiauto.foldable + given Foldable[EnumK1] = stackSafe.semiauto.foldable object traverseInst: - given Traverse[EnumK1] = semiauto.traverse + given Traverse[EnumK1] = stackSafe.semiauto.traverse object reducibleInst: - given Reducible[EnumK1] = semiauto.reducible + given Reducible[EnumK1] = stackSafe.semiauto.reducible object netInst: - given NonEmptyTraverse[EnumK1] = semiauto.nonEmptyTraverse - - enum BiTree[+A, +B]: - case Leaf(a: A, b: B) - case Branch(l: BiTree[A, B], r: BiTree[A, B]) - - object BiTree: - given Bifunctor[BiTree] = semiauto.bifunctor - given Bifoldable[BiTree] = semiauto.bifoldable - given Bitraverse[BiTree] = semiauto.bitraverse + given NonEmptyTraverse[EnumK1] = stackSafe.semiauto.nonEmptyTraverse class StackSafetySuite extends KittensSuite: import ADTs.* @@ -58,9 +48,6 @@ class StackSafetySuite extends KittensSuite: def deepEnumK1(): EnumK1[Int] = (1 to Size).foldLeft[EnumK1[Int]](EnumK1.Leaf(0))((acc, i) => EnumK1.Rec(EnumK1.Leaf(i), acc)) - def deepBiTree(): BiTree[Int, Int] = - (1 to Size).foldLeft[BiTree[Int, Int]](BiTree.Leaf(0, 0))((acc, i) => BiTree.Branch(BiTree.Leaf(i, i), acc)) - test("DerivedEq stack safety on IList[Int]"): import eqInst.given assert(Eq[IList[Int]].eqv(deepIList(), deepIList())) @@ -104,16 +91,4 @@ class StackSafetySuite extends KittensSuite: import netInst.given assert(NonEmptyTraverse[EnumK1].map(deepEnumK1())(_ + 1) ne null) - test("DerivedBifunctor stack safety on recursive BiTree"): - assert(Bifunctor[BiTree].bimap(deepBiTree())(_ + 1, _ + 1) ne null) - - test("DerivedBifoldable stack safety on recursive BiTree"): - val r = Bifoldable[BiTree].bifoldLeft(deepBiTree(), 0)(_ + _, _ + _) - assert(r != Int.MinValue || r == Int.MinValue) - - test("DerivedBitraverse stack safety on recursive BiTree"): - import cats.Eval - val r = Bitraverse[BiTree].bitraverse[Eval, Int, Int, Int, Int](deepBiTree())(Eval.now(_), Eval.now(_)).value - assert(r ne null) - end StackSafetySuite