From 8239eb9b0f360c2e8abfb76345fb3ef81635e8a1 Mon Sep 17 00:00:00 2001 From: Hugo van Rijswijk Date: Wed, 25 Oct 2023 16:45:30 +0200 Subject: [PATCH] Add Invariant and Semigroupal typeclasses for ViewF --- shared/src/main/scala/crystal/package.scala | 10 +++++++ shared/src/main/scala/crystal/syntax.scala | 1 + .../test/scala/crystal/ViewFLawsSpec.scala | 28 +++++++++++++++++++ .../src/test/scala/crystal/arbitraries.scala | 5 ++++ 4 files changed, 44 insertions(+) create mode 100644 shared/src/test/scala/crystal/ViewFLawsSpec.scala diff --git a/shared/src/main/scala/crystal/package.scala b/shared/src/main/scala/crystal/package.scala index d2e43d33..630aae5b 100644 --- a/shared/src/main/scala/crystal/package.scala +++ b/shared/src/main/scala/crystal/package.scala @@ -6,7 +6,9 @@ package crystal import cats.Applicative import cats.Eq import cats.FlatMap +import cats.Invariant import cats.Monad +import cats.Semigroupal import cats.effect.Ref import cats.syntax.all.* @@ -36,3 +38,11 @@ object throwable { (x ne null) == (y ne null) } } + +given [F[_]: Monad]: Semigroupal[ViewF[F, *]] with + def product[A, B](fa: ViewF[F, A], fb: ViewF[F, B]): ViewF[F, (A, B)] = + ViewF.apply(fa.get -> fb.get, (f, cb) => cb(f(fa.get, fb.get))) + +given [F[_]: Monad]: Invariant[ViewF[F, *]] with + def imap[A, B](fa: ViewF[F, A])(f: A => B)(g: B => A): ViewF[F, B] = + fa.zoom(f)(mod => a => g(mod(f(a)))) diff --git a/shared/src/main/scala/crystal/syntax.scala b/shared/src/main/scala/crystal/syntax.scala index 3a894e68..aede2f66 100644 --- a/shared/src/main/scala/crystal/syntax.scala +++ b/shared/src/main/scala/crystal/syntax.scala @@ -28,6 +28,7 @@ trait syntax { def toPot: Pot[A] = Pot.fromTry[A](a) @targetName("tryToPotOption") def toPotOption: PotOption[A] = PotOption.fromTry(a) + } object syntax extends syntax diff --git a/shared/src/test/scala/crystal/ViewFLawsSpec.scala b/shared/src/test/scala/crystal/ViewFLawsSpec.scala new file mode 100644 index 00000000..000b9713 --- /dev/null +++ b/shared/src/test/scala/crystal/ViewFLawsSpec.scala @@ -0,0 +1,28 @@ +// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA) +// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause + +package crystal + +import cats.Eq +import cats.Id +import cats.Invariant +import cats.laws.discipline.InvariantTests +import cats.laws.discipline.SemigroupalTests +import munit.DisciplineSuite + +import arbitraries.given + +class ViewFLawsSpec extends DisciplineSuite { + + private given viewEq[A: Eq]: Eq[ViewF[Id, A]] = Eq.by(_.get) + + checkAll( + "ViewF[Int].InvariantLaws", + InvariantTests[ViewF[Id, *]].invariant[Int, Int, String] + ) + + checkAll( + "ViewF[Int].SemigroupalLaws", + SemigroupalTests[ViewF[Id, *]].semigroupal[Int, Int, String] + ) +} diff --git a/shared/src/test/scala/crystal/arbitraries.scala b/shared/src/test/scala/crystal/arbitraries.scala index 16901efa..a056abde 100644 --- a/shared/src/test/scala/crystal/arbitraries.scala +++ b/shared/src/test/scala/crystal/arbitraries.scala @@ -3,6 +3,7 @@ package crystal +import cats.Id import org.scalacheck.* import scala.annotation.targetName @@ -33,6 +34,10 @@ object arbitraries { ) ) + given viewFArb[A: Arbitrary]: Arbitrary[ViewF[Id, A]] = Arbitrary( + arbitrary[A].map(a => ViewF.apply[Id, A](a, (f, cb) => cb(f(a)))) + ) + @targetName("potOptionFnArbitrary") given [A](using fArb: Arbitrary[A => A]): Arbitrary[PotOption[A] => PotOption[A]] = Arbitrary(arbitrary[A => A].map(f => _.map(f)))