diff --git a/build.sbt b/build.sbt index 8bd4d40433..03a0605745 100644 --- a/build.sbt +++ b/build.sbt @@ -445,7 +445,8 @@ lazy val kernel = crossProject(JSPlatform, JVMPlatform, NativePlatform) .settings( name := "cats-effect-kernel", libraryDependencies ++= Seq( - "org.typelevel" %%% "cats-core" % CatsVersion + "org.typelevel" %%% "cats-core" % CatsVersion, + "org.typelevel" %%% "cats-mtl" % CatsMtlVersion ), mimaBinaryIssueFilters ++= Seq( ProblemFilters.exclude[MissingClassProblem]("cats.effect.kernel.Ref$SyncRef"), @@ -501,7 +502,9 @@ lazy val laws = crossProject(JSPlatform, JVMPlatform, NativePlatform) name := "cats-effect-laws", libraryDependencies ++= Seq( "org.typelevel" %%% "cats-laws" % CatsVersion, - "org.typelevel" %%% "discipline-munit" % DisciplineMUnitVersion % Test) + "org.typelevel" %%% "cats-mtl-laws" % CatsMtlVersion % Test, + "org.typelevel" %%% "discipline-munit" % DisciplineMUnitVersion % Test + ) ) /** diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala b/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala index 7299b7afdb..b0614eda9f 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala @@ -21,6 +21,7 @@ import cats.data.Kleisli import cats.effect.kernel.Resource.Pure import cats.effect.kernel.implicits._ import cats.effect.kernel.instances.spawn +import cats.mtl.{LiftKind, LiftValue} import cats.syntax.all._ import scala.annotation.tailrec @@ -1275,6 +1276,10 @@ private[effect] trait ResourceHOInstances0 extends ResourceHOInstances1 { def K = K0 def G = G0 } + + implicit def catsEffectLiftKindForResource[F[_]]( + implicit F: MonadCancel[F, ?]): LiftKind[F, Resource[F, *]] = + liftKindImpl(F) } private[effect] trait ResourceHOInstances1 extends ResourceHOInstances2 { @@ -1289,6 +1294,25 @@ private[effect] trait ResourceHOInstances1 extends ResourceHOInstances2 { def F = F0 def rootCancelScope = F0.rootCancelScope } + + protected[this] def liftKindImpl[F[_]](F: MonadCancel[F, ?]): LiftKind[F, Resource[F, *]] = + new LiftKind[F, Resource[F, *]] { + implicit val applicativeF: MonadCancel[F, ?] = F + val applicativeG: Applicative[Resource[F, *]] = catsEffectMonadForResource + def apply[A](fa: F[A]): Resource[F, A] = Resource.eval(fa) + def limitedMapK[A](ga: Resource[F, A])(scope: F ~> F): Resource[F, A] = + ga.mapK(scope) + } + + implicit def catsEffectLiftKindForResourceComposed[F[_], G[_]]( + implicit inner: LiftKind[F, G], + G: MonadCancel[G, ?] + ): LiftKind[F, Resource[G, *]] = + inner.andThen(liftKindImpl(G)) + + implicit def catsEffectLiftValueForResource[F[_]]( + implicit F: Applicative[F]): LiftValue[F, Resource[F, *]] = + liftValueImpl(F) } private[effect] trait ResourceHOInstances2 extends ResourceHOInstances3 { @@ -1308,6 +1332,18 @@ private[effect] trait ResourceHOInstances2 extends ResourceHOInstances3 { final implicit def catsEffectDeferForResource[F[_]]: Defer[Resource[F, *]] = new ResourceDefer[F] + + protected[this] def liftValueImpl[F[_]](F: Applicative[F]): LiftValue[F, Resource[F, *]] = + new LiftValue[F, Resource[F, *]] { + val applicativeF: Applicative[F] = F + val applicativeG: Applicative[Resource[F, *]] = catsEffectMonadForResource + def apply[A](fa: F[A]): Resource[F, A] = Resource.eval(fa) + } + + implicit def catsEffectLiftValueForResourceComposed[F[_], G[_]]( + implicit inner: LiftValue[F, G] + ): LiftValue[F, Resource[G, *]] = + inner.andThen(liftValueImpl(inner.applicativeG)) } private[effect] trait ResourceHOInstances3 extends ResourceHOInstances4 { diff --git a/laws/shared/src/test/scala/cats/effect/kernel/ResourceSuite.scala b/laws/shared/src/test/scala/cats/effect/kernel/ResourceSuite.scala new file mode 100644 index 0000000000..5ccb4eaedd --- /dev/null +++ b/laws/shared/src/test/scala/cats/effect/kernel/ResourceSuite.scala @@ -0,0 +1,82 @@ +package cats +package effect +package kernel + +import cats.data.OptionT +import cats.effect.kernel.testkit.PureConcGenerators._ +import cats.effect.kernel.testkit.pure._ +import cats.laws.discipline.arbitrary.catsLawsArbitraryForOptionT +import cats.mtl.laws.discipline.{LiftKindTests, LiftValueTests} +import cats.syntax.flatMap._ +import cats.syntax.functor._ + +import org.scalacheck.{Arbitrary, Gen} + +import munit.DisciplineSuite + +class ResourceSuite extends DisciplineSuite { + type PCT[A] = PureConc[Throwable, A] + + private[this] val counter: PCT[Ref[PCT, Long]] = Concurrent[PCT].ref(0) + + implicit val eqThrowable: Eq[Throwable] = Eq.fromUniversalEquals + + implicit val arbitraryScope: Arbitrary[PCT ~> PCT] = + Arbitrary { + Gen.const { + new (PCT ~> PCT) { + def apply[A](fa: PCT[A]): PCT[A] = + for { + ref <- counter + res <- ref.update(_ + 1) >> fa + } yield res + } + } + } + + implicit def arbitraryPCTUnit(): Arbitrary[PCT[Unit]] = + Arbitrary { + Gen.const { + counter.flatMap(_.update(_ * 10)).void + } + } + + implicit def eqResource[F[_], A]( + implicit F: MonadCancelThrow[F], + eqFAUnit: Eq[F[(A, Unit)]] + ): Eq[Resource[F, A]] = + Eq.by { + _.allocated.flatMap { + case (acquire, release) => + release.map(acquire -> _) + } + } + + implicit def arbitraryResource[F[_]: Functor, A]( + implicit arbFA: Arbitrary[F[A]], + arbFUnit: Arbitrary[F[Unit]] + ): Arbitrary[Resource[F, A]] = + Arbitrary { + for { + acquire <- arbFA.arbitrary + release <- arbFUnit.arbitrary + } yield Resource(acquire.map(_ -> release)) + } + + checkAll( + "LiftValue[PureConc, Resource[PureConc, *]]", + LiftValueTests[PCT, Resource[PCT, *]].liftValue[Int, Int] + ) + checkAll( + "LiftValue[PureConc, Resource[OptionT[PureConc, *], *]]", + LiftValueTests[PCT, Resource[OptionT[PCT, *], *]].liftValue[Int, Int] + ) + checkAll( + "LiftKind[PureConc, Resource[PureConc, *]]", + LiftKindTests[PCT, Resource[PCT, *]].liftKind[Int, Int] + ) + checkAll( + "LiftKind[PureConc, Resource[OptionT[PureConc, *], *]]", + LiftKindTests[PCT, Resource[OptionT[PCT, *], *]].liftKind[Int, Int] + ) +}