diff --git a/build.sbt b/build.sbt index 21f88cf130..2ff7fcc61b 100644 --- a/build.sbt +++ b/build.sbt @@ -301,6 +301,7 @@ ThisBuild / apiURL := Some(url("https://typelevel.org/cats-effect/api/3.x/")) ThisBuild / autoAPIMappings := true val CatsVersion = "2.11.0" +val CatsMtlVersion = "1.3.0" val Specs2Version = "4.20.5" val ScalaCheckVersion = "1.17.1" val DisciplineVersion = "1.4.0" @@ -463,6 +464,9 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) .dependsOn(kernel, std) .settings( name := "cats-effect", + libraryDependencies ++= Seq( + "org.typelevel" %%% "cats-mtl" % CatsMtlVersion + ), mimaBinaryIssueFilters ++= Seq( // introduced by #1837, removal of package private class ProblemFilters.exclude[MissingClassProblem]("cats.effect.AsyncPropagateCancelation"), @@ -902,7 +906,8 @@ lazy val tests: CrossProject = crossProject(JSPlatform, JVMPlatform, NativePlatf "org.scalacheck" %%% "scalacheck" % ScalaCheckVersion, "org.specs2" %%% "specs2-scalacheck" % Specs2Version % Test, "org.typelevel" %%% "discipline-specs2" % DisciplineVersion % Test, - "org.typelevel" %%% "cats-kernel-laws" % CatsVersion % Test + "org.typelevel" %%% "cats-kernel-laws" % CatsVersion % Test, + "org.typelevel" %%% "cats-mtl-laws" % CatsMtlVersion % Test ), githubWorkflowArtifactUpload := false ) diff --git a/core/shared/src/main/scala/cats/effect/IO.scala b/core/shared/src/main/scala/cats/effect/IO.scala index 578078f9d7..a4c597547c 100644 --- a/core/shared/src/main/scala/cats/effect/IO.scala +++ b/core/shared/src/main/scala/cats/effect/IO.scala @@ -1949,6 +1949,9 @@ object IO extends IOCompanionPlatform with IOLowPriorityImplicits with TuplePara case Failure(err) => raiseError(err) } + def local[E](e: E): IO[cats.mtl.Local[IO, E]] = + IOLocal(e).map(_.asLocal) + // instances implicit def showForIO[A: Show]: Show[IO[A]] = diff --git a/core/shared/src/main/scala/cats/effect/IOLocal.scala b/core/shared/src/main/scala/cats/effect/IOLocal.scala index c0bcb8132f..e045cdd625 100644 --- a/core/shared/src/main/scala/cats/effect/IOLocal.scala +++ b/core/shared/src/main/scala/cats/effect/IOLocal.scala @@ -16,7 +16,9 @@ package cats.effect +import cats.Applicative import cats.data.AndThen +import cats.mtl.Local /** * [[IOLocal]] provides a handy way of manipulating a context on different scopes. @@ -238,6 +240,17 @@ sealed trait IOLocal[A] extends IOLocalPlatform[A] { self => */ def lens[B](get: A => B)(set: A => B => A): IOLocal[B] + final def asLocal: Local[IO, A] = + new Local[IO, A] { + def applicative: Applicative[IO] = + IO.asyncForIO + + def ask[A2 >: A] = + self.get + + def local[B](iob: IO[B])(f: A => A): IO[B] = + self.modify(e => f(e) -> e).bracket(Function.const(iob))(self.set) + } } object IOLocal { @@ -312,5 +325,4 @@ object IOLocal { new IOLocalLens(underlying, getter, setter) } } - } diff --git a/tests/shared/src/test/scala/cats/effect/IOMtlLocalSpec.scala b/tests/shared/src/test/scala/cats/effect/IOMtlLocalSpec.scala new file mode 100644 index 0000000000..e5d9d43bcc --- /dev/null +++ b/tests/shared/src/test/scala/cats/effect/IOMtlLocalSpec.scala @@ -0,0 +1,41 @@ +/* + * Copyright 2020-2022 Typelevel + * + * 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 effect + +import cats.mtl.Local +import cats.mtl.laws.discipline._ + +import org.typelevel.discipline.specs2.mutable.Discipline + +import java.util.concurrent.CancellationException + +class IOMtlLocalSpec extends BaseSpec with Discipline { + sequential + + implicit val ticker: Ticker = Ticker() + + implicit val local: Local[IO, Int] = + // Don't try this at home + unsafeRun(IO.local(0)).fold( + throw new CancellationException("canceled"), + throw _, + _.get + ) + + checkAll("Local[IO, Int]", LocalTests[IO, Int].local[Int, Int]) +}