diff --git a/core/src/main/scala-3/cats/derived/DerivedContravariant.scala b/core/src/main/scala-3/cats/derived/DerivedContravariant.scala index 4c0a45cc..cd517cec 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.* @@ -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 @@ -34,13 +41,18 @@ 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 + // ---- Default: fast direct recursion ---- + private def generic[F[_]: InstancesOf[Contravariant]]: DerivedContravariant[F] = new Generic[Contravariant, F] {} @@ -52,3 +64,39 @@ object DerivedContravariant: 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 + + 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 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) + 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 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]]] + )) diff --git a/core/src/main/scala-3/cats/derived/DerivedEq.scala b/core/src/main/scala-3/cats/derived/DerivedEq.scala index cbbf69a7..9be56ee1 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.* @@ -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,8 @@ 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) @@ -45,3 +54,37 @@ object DerivedEq: 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 + + 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 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) => + val next = acc.flatMap: b => + if !b then Eval.now(false) else DerivedEq.safeEqv(eqt)(xt, yt) + Complete(false)(next)(next) + + 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) + ) diff --git a/core/src/main/scala-3/cats/derived/DerivedFoldable.scala b/core/src/main/scala-3/cats/derived/DerivedFoldable.scala index 95f97775..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,8 @@ 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)) @@ -65,3 +74,42 @@ object DerivedFoldable: 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 = + 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 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]) => + 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 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) + )) + + 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 7df5259c..0ad74fdb 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.* @@ -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 @@ -41,8 +48,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,6 +68,8 @@ 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] {} @@ -69,3 +81,39 @@ object DerivedFunctor: 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 + + 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 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) + 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 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]]] + )) diff --git a/core/src/main/scala-3/cats/derived/DerivedHash.scala b/core/src/main/scala-3/cats/derived/DerivedHash.scala index 32b92635..494cea2a 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,6 +23,13 @@ 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 @@ -42,6 +49,8 @@ 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]: @@ -62,3 +71,44 @@ object DerivedHash: 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 + + 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 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] = + val arity = x.productArity + val prefix = x.productPrefix.hashCode + if arity <= 0 then Eval.now(prefix) + else + 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 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))) diff --git a/core/src/main/scala-3/cats/derived/DerivedInvariant.scala b/core/src/main/scala-3/cats/derived/DerivedInvariant.scala index 46cd3407..857b5fe1 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.* @@ -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 @@ -34,13 +41,18 @@ 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 + // ---- Default: fast direct recursion ---- + private def generic[F[_]: InstancesOf[Invariant]]: DerivedInvariant[F] = new Generic[Invariant, F] {} @@ -52,3 +64,39 @@ object DerivedInvariant: 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 + + 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 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) + 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 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]]] + )) diff --git a/core/src/main/scala-3/cats/derived/DerivedNonEmptyTraverse.scala b/core/src/main/scala-3/cats/derived/DerivedNonEmptyTraverse.scala index d071acc0..c7781ff3 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.* @@ -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], @@ -66,18 +90,6 @@ object DerivedNonEmptyTraverse: 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))) - private type Alt[F[_]] = [A] =>> Either[F[A], A] - private given [F[_]](using F: Apply[F]): Applicative[Alt[F]] with - 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)) - object Strict: def product[F[_]: ProductInstancesOf[Traverse]](ev: NonEmptyTraverse[?]): DerivedNonEmptyTraverse[F] = new Product[Traverse, F](ev) @@ -93,3 +105,46 @@ object DerivedNonEmptyTraverse: ): DerivedNonEmptyTraverse[F] = given CoproductInstances[NonEmptyTraverse, F] = inst.unify new NonEmptyTraverse[F] with Coproduct[NonEmptyTraverse, F] {} + + // ---- Opt-in: stack-safe recursion via Eval ---- + + object StackSafe: + def product[F[_]: ProductInstancesOf[Traverse]](ev: NonEmptyTraverse[?]): DerivedNonEmptyTraverse[F] = + 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]) + + given coproduct[F[_]](using + inst: => CoproductInstances[NonEmptyTraverse |: Derived, F] + ): DerivedNonEmptyTraverse[F] = + given CoproductInstances[NonEmptyTraverse, F] = inst.unify + 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 57b2056e..f3d00787 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.* @@ -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,8 @@ 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): @@ -50,3 +59,38 @@ object DerivedOrder: 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 + + 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 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) => + val next = acc.flatMap: cmp => + if cmp != 0 then Eval.now(cmp) else DerivedOrder.safeCompare(ord)(t0, t1) + Complete(false)(next)(next) + + 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) + ) diff --git a/core/src/main/scala-3/cats/derived/DerivedPartialOrder.scala b/core/src/main/scala-3/cats/derived/DerivedPartialOrder.scala index 50903601..ffa38f85 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.* @@ -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,8 @@ 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): @@ -50,3 +59,38 @@ object DerivedPartialOrder: 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 + + 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 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) => + val next = acc.flatMap: cmp => + if cmp != 0.0 then Eval.now(cmp) else DerivedPartialOrder.safePartialCompare(ord)(t0, t1) + Complete(false)(next)(next) + + 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) + ) diff --git a/core/src/main/scala-3/cats/derived/DerivedReducible.scala b/core/src/main/scala-3/cats/derived/DerivedReducible.scala index bfd7aa82..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,8 @@ 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]: @@ -91,3 +101,68 @@ object DerivedReducible: 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 = + 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 SafeProduct[T[f[_]] <: Foldable[f], F[_]](@unused ev: Reducible[?])(using inst: ProductInstances[T, F]) + extends Safe[F], + DerivedFoldable.SafeProduct[T, F]: + + private val evalNone = Eval.now(None) + + private[derived] final override def safeReduceLeftTo[A, B](fa: F[A])(f: A => B)(g: (B, A) => B): Eval[B] = + inst + .foldLeft[A, Eval[Option[B]]](fa)(evalNone): + [f[_]] => + (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 + .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 SafeCoproduct[T[f[_]] <: Reducible[f], F[_]](using inst: CoproductInstances[T, F]) + extends Safe[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]) => + 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..6bc3330c 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,6 +22,13 @@ 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 @@ -41,6 +48,8 @@ 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 @@ -73,3 +82,49 @@ object DerivedShow: 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 + + 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 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 + val n = labels.size + if n <= 0 then Eval.now(prefix) + else + val sb = new StringBuilder(prefix) + sb.append('(') + 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 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))) diff --git a/core/src/main/scala-3/cats/derived/DerivedTraverse.scala b/core/src/main/scala-3/cats/derived/DerivedTraverse.scala index fcbb7eb2..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,8 @@ 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], @@ -72,3 +81,55 @@ object DerivedTraverse: 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]] = + 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 SafeProduct[T[f[_]] <: Traverse[f], F[_]](using inst: ProductInstances[T, F]) + extends Safe[F], + DerivedFunctor.SafeProduct[T, F], + DerivedFoldable.SafeProduct[T, 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 SafeCoproduct[T[f[_]] <: Traverse[f], F[_]](using inst: CoproductInstances[T, F]) + extends Safe[F], + DerivedFunctor.SafeCoproduct[T, F], + DerivedFoldable.SafeCoproduct[T, 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]]]] + ) 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 75cf7394..32b46755 100644 --- a/core/src/test/scala-3/cats/derived/ContravariantSuite.scala +++ b/core/src/test/scala-3/cats/derived/ContravariantSuite.scala @@ -56,6 +56,15 @@ class ContravariantSuite extends KittensSuite: validate("strict.semiauto.contravariant") testNoInstance("strict.semiauto.contravariant", "TopK") + locally: + given Contravariant[EnumK1Contra] = stackSafe.semiauto.contravariant + val Size = 10000 + 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) + 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..10ead8ae 100644 --- a/core/src/test/scala-3/cats/derived/FunctorSuite.scala +++ b/core/src/test/scala-3/cats/derived/FunctorSuite.scala @@ -63,6 +63,21 @@ class FunctorSuite extends KittensSuite: validate("strict.semiauto.functor") testNoInstance("strict.semiauto.functor", "TopK") + locally: + given Functor[IList] = stackSafe.semiauto.functor + given Functor[EnumK1] = stackSafe.semiauto.functor + val Size = 50000 + 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("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) + assert(mapped ne null) + locally: import derivedInstances.* val instance = "derived.functor" 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..996a04b4 --- /dev/null +++ b/core/src/test/scala-3/cats/derived/StackSafetySuite.scala @@ -0,0 +1,94 @@ +package cats.derived + +import cats.{Eq, Hash, Invariant, Order, PartialOrder, Show} +import cats.{Foldable, NonEmptyTraverse, Reducible, Traverse} + +object StackSafetySuite: + import ADTs.* + + object eqInst: + given Eq[IList[Int]] = stackSafe.semiauto.eq + + object orderInst: + given Order[IList[Int]] = stackSafe.semiauto.order + + object pOrderInst: + given PartialOrder[IList[Int]] = stackSafe.semiauto.partialOrder + + object hashInst: + given Hash[IList[Int]] = stackSafe.semiauto.hash + + object showInst: + given Show[IList[Int]] = stackSafe.semiauto.show + + object invariantInst: + given Invariant[EnumK1] = stackSafe.semiauto.invariant + + object foldableInst: + given Foldable[EnumK1] = stackSafe.semiauto.foldable + + object traverseInst: + given Traverse[EnumK1] = stackSafe.semiauto.traverse + + object reducibleInst: + given Reducible[EnumK1] = stackSafe.semiauto.reducible + + object netInst: + given NonEmptyTraverse[EnumK1] = stackSafe.semiauto.nonEmptyTraverse + +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)) + + 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) + +end StackSafetySuite