With cats-effect 1.0.0 out, this library is now redundant. The Resource data type provides equivalent functionality.
ManagedT[F[_], A] is a monad for constructing computations that acquire and
release resources. It is a translation of Gabriel Gonzalez'
managed library, with the
difference that this library abstracts over the effect type.
This library only contains typeclass instances for
cats. Scalaz already contains the Codensity data
type which can serve the same functionality (with, albeit, some additional
plumbing for cleanup handlers).
This library supports any F[_] that has MonadError[F, Throwable]. That
includes monix.eval.Task, cats.effect.IO, scala.concurrent.Future (and
others).
However, it is very likely to be broken if:
- you use cancellation with
TaskorIO-MonadErroris not strong enough to express bracketing with cancellation, so that'll have to wait forMonadBracketincats-effect; - you use it with a monad transformer such as
EitherT[IO, E, A], where there are several "layers" in which errors could be thrown; - you throw exceptions in the acquire or cleanup actions that are not sequenced
into the
F[_]; - you pass
Futurevalues that have already been executed in the acquire or cleanup functions. The library has constructors with by-name values in most places to assist with that.
Other than that, it's all good ;-) The test suite exercises the common usecases; you are welcome to have a look.
libraryDependencies += ״com.iravid" %% "managedt" % "0.1"First, create some resources:
import com.iravid.managedt.ManagedT
// import com.iravid.managedt.ManagedT
import monix.eval.Task
// import monix.eval.Task
val resource1 = ManagedT(Task { println("Acquiring 1"); 1 })(r => Task(println(s"Cleaning $r")))
// resource1: com.iravid.managedt.ManagedT[monix.eval.Task,Int] = com.iravid.managedt.ManagedT$$anon$4@23497a65
val resource2 = ManagedT(Task { println("Acquiring 2"); 2 })(r => Task(println(s"Cleaning $r")))
// resource2: com.iravid.managedt.ManagedT[monix.eval.Task,Int] = com.iravid.managedt.ManagedT$$anon$4@42716335Compose them - ManagedT[F, R] is a monad in R, so you can use the usual forms of composition:
import cats.implicits._
// import cats.implicits._
val zipped = (resource1, resource2).tupled
// zipped: com.iravid.managedt.ManagedT[monix.eval.Task,(Int, Int)] = com.iravid.managedt.ManagedT$$anon$1$$anon$5@6d555633
val added = for {
r1 <- resource1
r2 <- resource2
} yield r1 + r2
// added: com.iravid.managedt.ManagedT[monix.eval.Task,Int] = com.iravid.managedt.ManagedT$$anon$1$$anon$5@228720d0
val resources = (1 to 10).toList traverse { r =>
ManagedT(Task { println(s"Acquiring $r"); r})(r => Task(println(s"Cleaning $r")))
}
// resources: com.iravid.managedt.ManagedT[monix.eval.Task,List[Int]] = com.iravid.managedt.ManagedT$$anon$1$$anon$5@346d3ee3And, finally, use your acquired resource in a function of the form R => F[A]:
val zippedUsage = zipped { case (r1, r2) =>
Task {
println("Using zipped"); r1 + r2
}
}
// zippedUsage: monix.eval.Task[Int] = Task.FlatMap$1596984844
val addedUsage = added { r =>
Task {
println("Using added"); r * r
}
}
// addedUsage: monix.eval.Task[Int] = Task.FlatMap$757626346
val resourcesUsage = resources { rs =>
Task {
println("Using resources")
rs.sum
}
}
// resourcesUsage: monix.eval.Task[Int] = Task.FlatMap$1046714810As we're using Task, nothing has actually run yet; we've composed programs that use the resources safely. Let's see what happens when we run them:
import monix.execution.Scheduler
implicit val scheduler = Scheduler.singleThread(name="tut")import scala.concurrent.Await
// import scala.concurrent.Await
import scala.concurrent.duration._
// import scala.concurrent.duration._
Await.result(zippedUsage.runAsync, 5.seconds)
// Acquiring 1
// Acquiring 2
// Using zipped
// Cleaning 2
// Cleaning 1
// res0: Int = 3The program constructed by ManagedT[F, A] properly acquires and releases resources in the right order. This also works for the traversed list of resources:
Await.result(resourcesUsage.runAsync, 5.seconds)
// Acquiring 1
// Acquiring 2
// Acquiring 3
// Acquiring 4
// Acquiring 5
// Acquiring 6
// Acquiring 7
// Acquiring 8
// Acquiring 9
// Acquiring 10
// Using resources
// Cleaning 10
// Cleaning 9
// Cleaning 8
// Cleaning 7
// Cleaning 6
// Cleaning 5
// Cleaning 4
// Cleaning 3
// Cleaning 2
// Cleaning 1
// res1: Int = 55Should one of the acquires, clean-ups or uses fail, the clean-ups will still run properly:
val acquireFailure = for {
_ <- ManagedT(Task.unit)(_ => Task { println("Cleaning 1") })
_ <- ManagedT(Task.unit)(_ => Task { println("Cleaning 2")})
_ <- ManagedT(Task.raiseError(new Exception))((_: Unit) => Task { println("Cleaning 3") })
} yield ()
// acquireFailure: com.iravid.managedt.ManagedT[monix.eval.Task,Unit] = com.iravid.managedt.ManagedT$$anon$1$$anon$5@de37d32
Await.result(acquireFailure(_ => Task.unit).attempt.runAsync, 5.seconds)
// Cleaning 2
// Cleaning 1
// res2: Either[Throwable,Unit] = Left(java.lang.Exception)
val useFailure = for {
_ <- ManagedT(Task.unit)(_ => Task { println("Cleaning 1") })
_ <- ManagedT(Task.unit)(_ => Task { println("Cleaning 2")})
_ <- ManagedT(Task.unit)(_ => Task { println("Cleaning 3") })
} yield ()
// useFailure: com.iravid.managedt.ManagedT[monix.eval.Task,Unit] = com.iravid.managedt.ManagedT$$anon$1$$anon$5@6c1de753
Await.result(useFailure(_ => Task.raiseError(new Exception())).attempt.runAsync, 5.seconds)
// Cleaning 3
// Cleaning 2
// Cleaning 1
// res3: Either[Throwable,Nothing] = Left(java.lang.Exception)
val cleanupFailure = for {
_ <- ManagedT(Task.unit)(_ => Task { println("Cleaning 1") })
_ <- ManagedT(Task.unit)(_ => Task.raiseError(new Exception()))
_ <- ManagedT(Task.unit)(_ => Task { println("Cleaning 3") })
} yield ()
// cleanupFailure: com.iravid.managedt.ManagedT[monix.eval.Task,Unit] = com.iravid.managedt.ManagedT$$anon$1$$anon$5@1417b95d
Await.result(cleanupFailure(_ => Task.unit).attempt.runAsync, 5.seconds)
// Cleaning 3
// Cleaning 1
// res4: Either[Throwable,Unit] = Left(java.lang.Exception)