diff --git a/build.sbt b/build.sbt index 2e140de51b..badb7fa5da 100644 --- a/build.sbt +++ b/build.sbt @@ -292,6 +292,7 @@ ThisBuild / apiURL := Some(url("https://typelevel.org/cats-effect/api/3.x/")) ThisBuild / autoAPIMappings := true val CatsVersion = "2.9.0" +val CatsMtlVersion = "1.3.0" val Specs2Version = "4.19.2" val ScalaCheckVersion = "1.17.0" val DisciplineVersion = "1.4.0" @@ -445,6 +446,7 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) .settings( name := "cats-effect", libraryDependencies ++= Seq( + "org.typelevel" %% "cats-mtl" % CatsMtlVersion, "org.typelevel" %% "scalac-compat-annotation" % ScalacCompatVersion % CompileTime ), mimaBinaryIssueFilters ++= Seq( @@ -833,7 +835,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 ), buildInfoPackage := "catseffect" ) diff --git a/core/shared/src/main/scala/cats/effect/IOLocal.scala b/core/shared/src/main/scala/cats/effect/IOLocal.scala index 0675fe042a..423012b6a4 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. @@ -243,6 +245,17 @@ sealed trait IOLocal[A] { self => } } + final def toLocal: 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 { @@ -319,4 +332,6 @@ object IOLocal { underlying.get.flatMap(s => underlying.reset.as(getter(s))) } + def local[E](e: E): IO[Local[IO, E]] = + IOLocal(e).map(_.toLocal) } diff --git a/tests/shared/src/test/scala/cats/effect/IOLocalLocalSpec.scala b/tests/shared/src/test/scala/cats/effect/IOLocalLocalSpec.scala new file mode 100644 index 0000000000..05898119c9 --- /dev/null +++ b/tests/shared/src/test/scala/cats/effect/IOLocalLocalSpec.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 IOLocalLocalSpec extends BaseSpec with Discipline { + sequential + + implicit val ticker = Ticker() + + implicit val local: Local[IO, Int] = + // Don't try this at home + unsafeRun(IOLocal.local(0)).fold( + throw new CancellationException("canceled"), + throw _, + _.get + ) + + checkAll("Local[IO, Int]", LocalTests[IO, Int].local[Int, Int]) +}