Skip to content

Commit

Permalink
Port invariant to new scheme (#478)
Browse files Browse the repository at this point in the history
* Port invariant derivation to new scheme

* Port scala 2 invariant tests to scala 3

* Consistent test naming

* WIP extra Invariant instances

* Tests for invariant derivation syntax
  • Loading branch information
TimWSpence authored May 29, 2022
1 parent 1096075 commit 5de791e
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 26 deletions.
37 changes: 37 additions & 0 deletions core/src/main/scala-3/cats/derived/DerivedInvariant.scala
Original file line number Diff line number Diff line change
@@ -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)
)
14 changes: 0 additions & 14 deletions core/src/main/scala-3/cats/derived/invariant.scala

This file was deleted.

7 changes: 6 additions & 1 deletion core/src/main/scala-3/cats/derived/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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:
Expand Down Expand Up @@ -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]
101 changes: 101 additions & 0 deletions core/src/test/scala-3/cats/derived/InvariantSuite.scala
Original file line number Diff line number Diff line change
@@ -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]])
11 changes: 0 additions & 11 deletions core/src/test/scala-3/cats/derived/InvariantTests.scala

This file was deleted.

0 comments on commit 5de791e

Please sign in to comment.