diff --git a/core/src/main/scala-3/cats/derived/DerivedInvariant.scala b/core/src/main/scala-3/cats/derived/DerivedInvariant.scala new file mode 100644 index 00000000..41b1be98 --- /dev/null +++ b/core/src/main/scala-3/cats/derived/DerivedInvariant.scala @@ -0,0 +1,37 @@ +package cats.derived + +import cats.{Contravariant, Functor, Invariant} +import shapeless3.deriving.{Const, K1} + +import scala.annotation.implicitNotFound +import scala.compiletime.* +import scala.util.NotGiven + +@implicitNotFound("""Could not derive an instance of Invariant[F] where F = ${F}. +Make sure that F[_] satisfies one of the following conditions: + * it is a constant type [x] => T + * it is a nested type [x] => G[H[x]] where G: Invariant and H: Invariant + * it is a generic case class where all fields have an Invariant instance + * it is a generic sealed trait where all subclasses have an Invariant instance""") +type DerivedInvariant[F[_]] = Derived[Invariant[F]] +object DerivedInvariant: + type Or[F[_]] = Derived.Or[Invariant[F]] + inline def apply[F[_]]: Invariant[F] = + import DerivedInvariant.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 + + given [F[_], G[_]](using F: Or[F], G: Or[G]): DerivedInvariant[[x] =>> F[G[x]]] = + given Invariant[G] = G.unify + F.unify.compose[G] + + given [F[_]](using inst: => K1.Instances[Or, F]): DerivedInvariant[F] = + given K1.Instances[Invariant, F] = inst.unify + new Generic[Invariant, F] {} + + trait Generic[T[x[_]] <: Invariant[x], F[_]](using inst: K1.Instances[T, F]) extends Invariant[F]: + def imap[A, B](fa: F[A])(f: A => B)(g: B => A): F[B] = inst.map(fa)( + [t[_]] => (inv: T[t], t0: t[A]) => inv.imap(t0)(f)(g) + ) diff --git a/core/src/main/scala-3/cats/derived/invariant.scala b/core/src/main/scala-3/cats/derived/invariant.scala deleted file mode 100644 index 74d9dcc4..00000000 --- a/core/src/main/scala-3/cats/derived/invariant.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cats.derived - -import cats.Invariant -import shapeless3.deriving.K1 - -trait GenericInvariant[T[x[_]] <: Invariant[x], F[_]](using inst: K1.Instances[T, F]) extends Invariant[F]: - def imap[A, B](fa: F[A])(f: A => B)(g: B => A): F[B] = inst.map(fa)( - [t[_]] => (inv: T[t], t0: t[A]) => inv.imap(t0)(f)(g) - ) - -trait InvariantDerivation: - extension (F: Invariant.type) - inline def derived[F[_]](using gen: K1.Generic[F]): Invariant[F] = - new GenericInvariant[Invariant, F] {} diff --git a/core/src/main/scala-3/cats/derived/package.scala b/core/src/main/scala-3/cats/derived/package.scala index dd4fcee4..914bbe25 100644 --- a/core/src/main/scala-3/cats/derived/package.scala +++ b/core/src/main/scala-3/cats/derived/package.scala @@ -27,8 +27,9 @@ extension (x: NonEmptyTraverse.type) inline def derived[F[_]]: NonEmptyTraverse[ extension (x: SemigroupK.type) inline def derived[F[_]]: SemigroupK[F] = DerivedSemigroupK[F] extension (x: MonoidK.type) inline def derived[F[_]]: MonoidK[F] = DerivedMonoidK[F] extension (x: Contravariant.type) inline def derived[F[_]]: Contravariant[F] = DerivedContravariant[F] +extension (x: Invariant.type) inline def derived[F[_]]: Invariant[F] = DerivedInvariant[F] -object semiauto extends InvariantDerivation, PartialOrderDerivation, Instances: +object semiauto extends PartialOrderDerivation, Instances: inline def eq[A]: Eq[A] = DerivedEq[A] inline def hash[A]: Hash[A] = DerivedHash[A] @@ -51,6 +52,7 @@ object semiauto extends InvariantDerivation, PartialOrderDerivation, Instances: inline def semigroupK[F[_]]: SemigroupK[F] = DerivedSemigroupK[F] inline def monoidK[F[_]]: MonoidK[F] = DerivedMonoidK[F] inline def contravariant[F[_]]: Contravariant[F] = DerivedContravariant[F] + inline def invariant[F[_]]: Invariant[F] = DerivedInvariant[F] object auto: object eq: @@ -115,3 +117,6 @@ object auto: object contravariant: inline given [F[_]](using NotGiven[Contravariant[F]]): Contravariant[F] = DerivedContravariant[F] + + object invariant: + inline given [F[_]](using NotGiven[Invariant[F]]): Invariant[F] = DerivedInvariant[F] diff --git a/core/src/test/scala-3/cats/derived/InvariantSuite.scala b/core/src/test/scala-3/cats/derived/InvariantSuite.scala new file mode 100644 index 00000000..835285c7 --- /dev/null +++ b/core/src/test/scala-3/cats/derived/InvariantSuite.scala @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2015 Miles Sabin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats +package derived +import cats.laws.discipline.* +import cats.laws.discipline.arbitrary.* +import cats.laws.discipline.eq.* +import cats.laws.discipline.* + +import scala.compiletime.* + +class InvariantSuite extends KittensSuite: + import InvariantSuite.* + import TestDefns.* + + inline def invariantTests[F[_]]: InvariantTests[F] = InvariantTests[F](summonInline) + + inline def testInvariant(context: String): Unit = { + checkAll(s"$context.Invariant[TreeF]", invariantTests[TreeF].invariant[MiniInt, String, Boolean]) + checkAll(s"$context.Invariant[GenAdtF]", invariantTests[GenericAdtF].invariant[MiniInt, String, Boolean]) + // TODO https://github.com/typelevel/kittens/issues/473 + // checkAll(s"$context.Invariant[InterleavedF]", invariantTests[InterleavedF].invariant[MiniInt, String, Boolean]) + checkAll(s"$context.Invariant[AndCharF]", invariantTests[AndCharF].invariant[MiniInt, String, Boolean]) + checkAll(s"$context.Invariant[ListSnoc", invariantTests[ListSnoc].invariant[MiniInt, String, Boolean]) + checkAll(s"$context.Invariant[Bivariant]", invariantTests[Bivariant].invariant[MiniInt, String, Boolean]) + + checkAll(s"$context.Invariant is Serializable", SerializableTests.serializable(summonInline[Invariant[TreeF]])) + + // TODO https://github.com/typelevel/kittens/issues/476 + // test(s"$context.Invariant.imap is stack safe") { + // val I = summonInline[Invariant[ListSnoc]] + // val J = summonInline[Invariant[IList]] + // val n = 10000 + // val largeIList = IList.fromSeq(1 until n) + // val largeSnoc = Snoc.fromSeq(1 until n) :: Nil + // val actualIList = IList.toList(J.imap(largeIList)(_ + 1)(_ - 1)) + // val actualSnoc = I.imap(largeSnoc)(_ + 1)(_ - 1).flatMap(Snoc.toList) + // val expected = (2 until n + 1).toList + // assert(actualIList == expected) + // assert(actualSnoc == expected) + // } + } + + locally { + import auto.invariant.given + testInvariant("auto") + } + + locally { + import semiInstances.given + testInvariant("semiauto") + } + +object InvariantSuite: + import TestDefns.* + + type ListSnoc[A] = List[Snoc[A]] + type GenericAdtF[A] = GenericAdt[A => Boolean] + type ListFToInt[A] = List[Snoc[A => Int]] + type InterleavedF[A] = Interleaved[A => Boolean] + type AndCharF[A] = (A => Boolean, Char) + type TreeF[A] = Tree[A => Boolean] + + object semiInstances: + implicit val gadt: Invariant[GenericAdtF] = semiauto.invariant[GenericAdtF] + implicit val listSnocendo: Invariant[ListFToInt] = semiauto.invariant[ListFToInt] + // implicit val interleaveF: Invariant[InterleavedF] = semiauto.invariant[InterleavedF] + implicit val andCharF: Invariant[AndCharF] = semiauto.invariant[AndCharF] + implicit val treeF: Invariant[TreeF] = semiauto.invariant[TreeF] + implicit val pred: Invariant[Pred] = semiauto.invariant[Pred] + implicit val snoc: Invariant[ListSnoc] = semiauto.invariant[ListSnoc] + implicit val bivariant: Invariant[Bivariant] = semiauto.invariant[Bivariant] + implicit val ilist: Invariant[IList] = semiauto.invariant[IList] + + case class Single[A](value: A) derives Invariant + + enum Many[A] derives Invariant: + case Naught() + case More(value: A, rest: Many[A]) + + enum AtMostOne[A] derives Invariant: + case Naught() + case Single(value: A) + + enum AtLeastOne[A] derives Invariant: + case Single(value: A) + case More(value: A, rest: Option[AtLeastOne[A]]) diff --git a/core/src/test/scala-3/cats/derived/InvariantTests.scala b/core/src/test/scala-3/cats/derived/InvariantTests.scala deleted file mode 100644 index 71b1ac9f..00000000 --- a/core/src/test/scala-3/cats/derived/InvariantTests.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cats.derived - -import cats.Invariant -import cats.derived.semiauto.* -import cats.derived.semiauto.given - -class InvariantTests { - - case class Foo[A](bar: String, baz: Option[A]) derives Invariant - -}