diff --git a/README.md b/README.md index a885a33..587ca83 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ A Future based monad with typed errors. Designed to be a replacement for the `scala.concurrent.Future` -(`StdFuture`) with minimal migration needed. Entirely built on top -of the `StdFuture` it has -the same performance and easily integrates into existing `StdFuture` +(I'll call it StdFuture here) with minimal migration needed. Entirely built on top +of the StdFuture, it has +the same performance and easily integrates into existing StdFuture based libraries. -It also extends the api of the `StdFuture`, which is heavily +It also extends the api of the StdFuture, which is heavily inspired by ZIO ([github link](https://github.com/zio/zio)). If you are already used to working with typed errors I would highly @@ -16,135 +16,119 @@ However if you do not want to commit to another effect system and still want complete control of your types this library is for you. # Installation + > [!NOTE] -> Due to the new sonatype interace the library is not yet available -in maven central. +> Due to the new sonatype interface the library is not yet available +> in maven central. Setup via `build.sbt`: ```sbt -libraryDependencies += "io.github.ragazoor" %% "io" % "0.1.0" +libraryDependencies += "io.github.ragazoor" %% "future" % "0.1.0" ``` # Getting Started -Compile and or run test - -```shell -sbt compile -``` - -```shell -sbt test -``` - ## Examples ```scala -import scala.concurrent.{ExecutionContext, Future => StdFuture} +import common.{User, UserRepository} import io.github.ragazoor.Future -import io.github.ragazoor.implicits._ - -case class User(name: String, age: Int) - -trait UserRepository { - def getUser(id: Int): StdFuture[User] -} +import io.github.ragazoor.implicits.StdFutureToTask -class UserService(userRepo: UserRepository)(implicit ec: ExecutionContext) { - def getUser(id: Int): Future[User] = // Future[User] is an alias for IO[Throwable, User] +class UserServiceExample(userRepo: UserRepository) { + def getUser(id: Int): Future[User] = // Future[User] is an alias for Task[Throwable, User] userRepo .getUser(id) - .io // Converts to IO + .toTask // Converts to Task } ``` + In `io.github.ragazoor.migration.implicits._` there are implicits that -are used to convert an `IO` to a `Future`. This is useful in a migration -phase when you have a third party library which depends on getting a -`Future`. +are used to convert an `Task` to a `StdFuture`. This is useful in a migration +phase when you have a third party library which depends on getting a +`StdFuture`. ```scala -import scala.concurrent.{ExecutionContext, Future => StdFuture} +import common.User +import io.github.ragazoor.Task import io.github.ragazoor.Future -import io.github.ragazoor.implicits._ import io.github.ragazoor.migration.implicits._ +import io.github.ragazoor.implicits.StdFutureToTask -case class User(name: String, age: Int) +import scala.concurrent.{ExecutionContext, Future => StdFuture} -trait UserRepository { - def getUser(id: Int): IO[Exception, User] +/* + * Imagine this is in a third party library + */ +trait UserProcess { + def process(id: StdFuture[User]): StdFuture[User] } -class UserService(userRepo: UserRepository)(implicit ec: ExecutionContext) { - // implicit conversion in io.github.ragazoor.migration.implicits._ - // converts the IO to a Future - - def getUser(id: Int): StdFuture[User] = - userRepo.getUser(id) - +class UserServiceFutureExample(userProcess: UserProcess)(implicit ec: ExecutionContext) { + + /* implicit conversion in io.github.ragazoor.migration.implicits._ converts + * the Task to a Future + */ + def getUser(id: Int): Future[User] = + userProcess.process { + Task.successful(User("Test name", 44)) + }.toTask + // Does the same thing without implicits, but more migration needed - def getUserExplicit(id: Int): StdFuture[User] = - userRepo.getUser(id).toFuture + def getUserExplicit(id: Int): Future[User] = + userProcess.process { + Task.successful(User("Test name", 44)).toFuture // Here the conversion to future is explicit + }.toTask } + ``` -This is the basics for using the typed future in -your code. The `IO` has the same API -as the `Future`, and thanks to the type alias -`type Future[+A] = IO[Throwable, A]` we don't need to rename `Future`s +This is the basics for using the `Task` type in +your code. The Task has the same API +as the StdFuture, and thanks to the type alias +`type Future[+A] = Task[Throwable, A]` we don't need to rename StdFutures all over the code base. ### Error handling Using the example above it is now trivial to map a failed `StdFuture` -to an `IO` with an error from our domain model. +to an `Task` with an error from our domain model. ```scala -case class UserNotFound(message: String, cause: Throwable) extends Exception(message, cause) - -object UserNotFound { - def apply(id: Int)(cause: Throwable): UserNotFound = UserNotFound(s"User with id $id not found", cause) -} +import common.{User, UserNotFound, UserRepository} +import io.github.ragazoor.Task +import io.github.ragazoor.implicits.StdFutureToTask -class UserService(userRepo: UserRepository)(implicit ec: ExecutionContext) { - def getUser(id: Int): IO[UserNotFound, User] = // Future[User] is an alias for IO[Throwable, User] - userRepo - .getUser(id) - .io // Converts to IO - .mapError(UserNotFound(id)) -``` +import scala.concurrent.ExecutionContext -Similar to `ZIO` it is also possible to create IO's with errors that we cannot -recover from, except with a few methods like `catch` and `recover`. This is done by using `IO.fatal`: -```scala -case class UnrecoverableError(message: String, cause: Throwable) extends Exception(message, cause) - -class UserService(userRepo: UserRepository)(implicit ec: ExecutionContext) { - def getUser(id: Int): IO[UserNotFound, User] = - if (id < 0) { - IO.fatal(UnrecoverableError("Not best example but lets say this is a fatal error", new RuntimeException("Fatal error"))) - } else { - userRepo - .getUser(id) - .io // Converts to IO - .mapError(UserNotFound(id)) - } +class UserServiceTaskExample(userRepo: UserRepository)(implicit ec: ExecutionContext) { + def getUser(id: Int): Task[UserNotFound, User] = + userRepo + .getUser(id) + .toTask // Converts to Task + .mapError(e => UserNotFound(s"common.User with id $id not found", e)) // Converts Error from Throwable -> UserNotFound } ``` ## Migration -The goal is to eventually be able to replace `scala.concurrent`, however -not everything is available yet. If you are only using `StdFuture`, -`ExecutionContext` and `NonFatal` you can use the following to migrate -most of the code: +The goal of the library is not to replace everything in `scala.concurrent.*` +since this would require a re-implementation of several key components. The +goal is rather to provide a typed alternative to the Future and +use the rest from the standard library. + +The migration depends on how much of the `scala.concurrent` library you are +using. This example is for a migration where the project is only using +ExecutionContext and Future from `scala.concurrent`. ```text replace: -import.scala.concurrent.* +import scala.concurrent.* with: +import scala.concurrent.{ExecutionContext, Future => StdFuture} import io.github.ragazoor.* import io.github.ragazoor.implicits.* import io.github.ragazoor.migration.implicits.* @@ -153,24 +137,32 @@ import io.github.ragazoor.migration.implicits.* There are a few occurrences where we need to manually fix the code: - If we are using a third-party library returning a `scala.concurrent.Future` - we need to convert it to `IO` using `.io` and the implicit in - `ragazoor.implicits.*`. -- If there are async tests using `scala.concurrent.Future` but does not - have `scala.concurrent` in imported we need to add - `import io.github.ragazoor.migration.implicits.*`. -- If you are using implicit classes that uses the - `StdFuture` the compiler will not be able to convert + we need to convert it to `Task` using `.toTask` and the implicit + `io.github.ragazoor.implicits.StdFutureToTask`. +- If there are async tests using `StdFuture` but does not + have `scala.concurrent` imported we need to add + `import io.github.ragazoor.migration.implicits._`. +- If you have interfaces in your code like `A => StdFuture[B]` there are + implicits in `import io.github.ragazoor.migration.implicits._` which + help with this. +- If you are using implicit classes that extends `scala.concurrent.Future` + the compiler will not be able to convert like one might think using the migration implicits. So we need to make it explicit: ```scala -implicit class MyImplicitClassFunction(f: StdFuture[Int]) { - def bar: StdFuture[Option[Int]] = f.map(Some(_)) -} +object ImplicitClassExample { + implicit class MyImplicitClassFunction(f: StdFuture[Int])(implicit ec: ExecutionContext) { + def bar: StdFuture[Option[Int]] = f.map(Some(_)) + } -def foo: IO[Throwable, Int] = ??? -val a: IO[Throwable, Option[Int]] = foo.bar.io // does not compile -val a: IO[Throwable, Option[Int]] = foo.toFuture.bar.io // compiles + import scala.concurrent.ExecutionContext.Implicits.global + + def foo: Attempt[Throwable, Int] = ??? + + val a: Attempt[Throwable, Option[Int]] = foo.bar.attempt // does not compile + val b: Attempt[Throwable, Option[Int]] = foo.toFuture.bar.attempt +} ``` ## Benchmarks @@ -191,9 +183,9 @@ Example benchmark [info] FutureBenchmark.futureMap thrpt 10 27.629 ± 0.490 ops/s [info] FutureBenchmark.futureRecover thrpt 10 24.488 ± 0.415 ops/s [info] FutureBenchmark.futureSequence thrpt 10 2.004 ± 0.203 ops/s -[info] FutureBenchmark.ioFlatMap thrpt 10 22.395 ± 0.375 ops/s -[info] FutureBenchmark.ioMap thrpt 10 27.328 ± 0.455 ops/s -[info] FutureBenchmark.ioMapError thrpt 10 27.177 ± 0.041 ops/s -[info] FutureBenchmark.ioSequence thrpt 10 1.817 ± 0.029 ops/s +[info] FutureBenchmark.taskFlatMap thrpt 10 22.395 ± 0.375 ops/s +[info] FutureBenchmark.taskMap thrpt 10 27.328 ± 0.455 ops/s +[info] FutureBenchmark.taskMapError thrpt 10 27.177 ± 0.041 ops/s +[info] FutureBenchmark.taskSequence thrpt 10 1.817 ± 0.029 ops/s [success] Total time: 1623 s (27:03), completed Feb 20, 2024, 7:02:20 PM ``` diff --git a/benchmark/src/main/scala/io/github/ragazoor/IOBenchmark.scala b/benchmark/src/main/scala/io/github/ragazoor/IOBenchmark.scala index 74bc635..f250e6f 100644 --- a/benchmark/src/main/scala/io/github/ragazoor/IOBenchmark.scala +++ b/benchmark/src/main/scala/io/github/ragazoor/IOBenchmark.scala @@ -21,7 +21,7 @@ class IOBenchmark { r.get.isInstanceOf[Success[T]] } - protected final def await[E <: Throwable, T](result: IO[E, T]): Boolean = { + protected final def await[E <: Throwable, T](result: Task[E, T]): Boolean = { var r: Option[Try[T]] = None while (r eq None) r = result.value r.get.isInstanceOf[Success[T]] @@ -31,7 +31,7 @@ class IOBenchmark { await(StdFuture.sequence(input.map(StdFuture.successful))) @Benchmark def resultSequence: Boolean = - await(IO.sequence(input.map(IO.successful))) + await(Task.sequence(input.map(Task.successful))) @tailrec private[this] final def futureFlatMapRec(i: Int, f: StdFuture[Int])(implicit ec: ExecutionContext @@ -42,14 +42,14 @@ class IOBenchmark { @Benchmark final def futureFlatMap: Boolean = await(futureFlatMapRec(recursion, StdFuture.successful(1))) - @tailrec private[this] final def resultFlatMapRec(i: Int, f: IO[Nothing, Int])(implicit + @tailrec private[this] final def resultFlatMapRec(i: Int, f: Task[Nothing, Int])(implicit ec: ExecutionContext - ): IO[Nothing, Int] = - if (i > 0) resultFlatMapRec(i - 1, f.flatMap(IO.successful)(ec))(ec) + ): Task[Nothing, Int] = + if (i > 0) resultFlatMapRec(i - 1, f.flatMap(Task.successful)(ec))(ec) else f @Benchmark final def resultFlatMap: Boolean = - await(resultFlatMapRec(recursion, IO.successful(1))) + await(resultFlatMapRec(recursion, Task.successful(1))) @tailrec private[this] final def futureMapRec(i: Int, f: StdFuture[Int])(implicit ec: ExecutionContext @@ -60,14 +60,14 @@ class IOBenchmark { @Benchmark final def futureMap: Boolean = await(futureMapRec(recursion, StdFuture.successful(1))) - @tailrec private[this] final def resultMapRec(i: Int, f: IO[Nothing, Int])(implicit + @tailrec private[this] final def resultMapRec(i: Int, f: Task[Nothing, Int])(implicit ec: ExecutionContext - ): IO[Nothing, Int] = + ): Task[Nothing, Int] = if (i > 0) resultMapRec(i - 1, f.map(identity)(ec))(ec) else f @Benchmark final def resultMap: Boolean = - await(resultMapRec(recursion, IO.successful(1))) + await(resultMapRec(recursion, Task.successful(1))) @tailrec private[this] final def futureRecoverWithRec(i: Int, f: StdFuture[Int])(implicit ec: ExecutionContext @@ -75,9 +75,9 @@ class IOBenchmark { if (i > 0) futureRecoverWithRec(i - 1, f.recoverWith(e => StdFuture.failed(e))(ec))(ec) else f - @tailrec private[this] final def resultMapErrorRec(i: Int, f: IO[RuntimeException, Int])(implicit + @tailrec private[this] final def resultMapErrorRec(i: Int, f: Task[RuntimeException, Int])(implicit ec: ExecutionContext - ): IO[RuntimeException, Int] = + ): Task[RuntimeException, Int] = if (i > 0) resultMapErrorRec(i - 1, f.mapError(identity)(ec))(ec) else f @@ -85,5 +85,5 @@ class IOBenchmark { await(futureRecoverWithRec(recursion, StdFuture.failed[Int](new RuntimeException("Future error")))) @Benchmark final def resultMapError: Boolean = - await(resultMapErrorRec(recursion, IO.failed(new RuntimeException("Result error")))) + await(resultMapErrorRec(recursion, Task.failed(new RuntimeException("Result error")))) } diff --git a/build.sbt b/build.sbt index 26943a0..3a690c7 100644 --- a/build.sbt +++ b/build.sbt @@ -13,17 +13,25 @@ lazy val root = project .in(file(".")) .settings(publish / skip := true) - .aggregate(result, benchmark) + .aggregate(result, benchmark, examples) -lazy val result = module("typed-future-io", "result") +lazy val result = module("attempt", "result") .enablePlugins(BuildInfoPlugin) - .settings(buildInfoSettings("ragz")) + .settings(buildInfoSettings("io.github.ragazoor")) .settings(libraryDependencies += munit % Test) - .settings(stdSettings("io")) + .settings(stdSettings("attempt")) + +lazy val examples = module("examples", "examples") + .enablePlugins(BuildInfoPlugin) + .settings(buildInfoSettings("io.github.ragazoor")) + .settings(libraryDependencies += munit % Test) + .settings(publish / skip := true) + .settings(stdSettings("examples")) + .dependsOn(result) lazy val benchmark = module("typed-future-benchmark", "benchmark") .enablePlugins(BuildInfoPlugin, JmhPlugin) - .settings(buildInfoSettings("ragz")) + .settings(buildInfoSettings("io.github.ragazoor")) .settings(libraryDependencies += munit % Test) .settings(publish / skip := true) .settings(stdSettings("benchmark")) diff --git a/examples/src/main/scala/common/User.scala b/examples/src/main/scala/common/User.scala new file mode 100644 index 0000000..14a9e7e --- /dev/null +++ b/examples/src/main/scala/common/User.scala @@ -0,0 +1,3 @@ +package common + +final case class User(name: String, age: Int) diff --git a/examples/src/main/scala/common/UserNotFound.scala b/examples/src/main/scala/common/UserNotFound.scala new file mode 100644 index 0000000..84116f0 --- /dev/null +++ b/examples/src/main/scala/common/UserNotFound.scala @@ -0,0 +1,3 @@ +package common + +final case class UserNotFound(msg: String, cause: Throwable) extends Exception(msg, cause) diff --git a/examples/src/main/scala/common/UserRepository.scala b/examples/src/main/scala/common/UserRepository.scala new file mode 100644 index 0000000..3d52b40 --- /dev/null +++ b/examples/src/main/scala/common/UserRepository.scala @@ -0,0 +1,6 @@ +package common + +import scala.concurrent.{ Future => StdFuture } +trait UserRepository { + def getUser(id: Int): StdFuture[User] +} diff --git a/examples/src/main/scala/examples/ImplicitClassExample.scala b/examples/src/main/scala/examples/ImplicitClassExample.scala new file mode 100644 index 0000000..6e4da34 --- /dev/null +++ b/examples/src/main/scala/examples/ImplicitClassExample.scala @@ -0,0 +1,18 @@ +package examples + +import io.github.ragazoor.Task +import io.github.ragazoor.implicits.StdFutureToTask + +import scala.concurrent.{ ExecutionContext, Future => StdFuture } + +object ImplicitClassExample { + implicit class MyImplicitClassFunction[A](f: StdFuture[A])(implicit ec: ExecutionContext) { + def bar: StdFuture[Option[A]] = f.map(Some(_)) + } + def foo: Task[Throwable, Int] = ??? + /* does not compile */ + // val a: Task[Throwable, Option[Int]] = foo.bar.toTask + + import scala.concurrent.ExecutionContext.Implicits.global + val b: Task[Throwable, Option[Int]] = foo.toFuture.bar.toTask +} diff --git a/examples/src/main/scala/examples/UserServiceExample.scala b/examples/src/main/scala/examples/UserServiceExample.scala new file mode 100644 index 0000000..1d78b79 --- /dev/null +++ b/examples/src/main/scala/examples/UserServiceExample.scala @@ -0,0 +1,12 @@ +package examples + +import common.{ User, UserRepository } +import io.github.ragazoor.Future +import io.github.ragazoor.implicits.StdFutureToTask + +class UserServiceExample(userRepo: UserRepository) { + def getUser(id: Int): Future[User] = // Future[User] is an alias for Task[Throwable, User] + userRepo + .getUser(id) + .toTask // Converts to Task +} diff --git a/examples/src/main/scala/examples/UserServiceFutureExample.scala b/examples/src/main/scala/examples/UserServiceFutureExample.scala new file mode 100644 index 0000000..80457b3 --- /dev/null +++ b/examples/src/main/scala/examples/UserServiceFutureExample.scala @@ -0,0 +1,28 @@ +package examples + +import common.User +import io.github.ragazoor.Future +import io.github.ragazoor.implicits.StdFutureToTask +import io.github.ragazoor.migration.implicits._ + +import scala.concurrent.{ Future => StdFuture } + +/* + * Imagine this is in a third party library + */ +trait UserProcess { + def process(id: StdFuture[User]): StdFuture[User] +} + +class UserServiceFutureExample(userProcess: UserProcess) { + + /* implicit conversion in io.github.ragazoor.migration.implicits._ converts + * the Task to a Future + */ + def proccessUser(user: Future[User]): Future[User] = + userProcess.process(user).toTask + + // Does the same thing without implicits, but more migration needed + def getUserExplicit(user: Future[User]): Future[User] = + userProcess.process(user.toFuture).toTask +} diff --git a/examples/src/main/scala/examples/UserServiceTaskExample.scala b/examples/src/main/scala/examples/UserServiceTaskExample.scala new file mode 100644 index 0000000..e03c470 --- /dev/null +++ b/examples/src/main/scala/examples/UserServiceTaskExample.scala @@ -0,0 +1,17 @@ +package examples + +import common.{ User, UserNotFound, UserRepository } +import io.github.ragazoor.Task +import io.github.ragazoor.implicits.StdFutureToTask + +import scala.concurrent.ExecutionContext + +class UserServiceTaskExample(userRepo: UserRepository)(implicit ec: ExecutionContext) { + def getUser(id: Int): Task[UserNotFound, User] = + userRepo + .getUser(id) + .toTask // Converts to Task + .mapError(e => + UserNotFound(s"common.User with id $id not found", e) + ) // Converts Error from Throwable -> UserNotFound +} diff --git a/result/src/main/scala/io/github/ragazoor/Future.scala b/result/src/main/scala/io/github/ragazoor/Future.scala index 6b0e33c..b215f91 100644 --- a/result/src/main/scala/io/github/ragazoor/Future.scala +++ b/result/src/main/scala/io/github/ragazoor/Future.scala @@ -8,32 +8,32 @@ import scala.util.Try */ object Future { - def unapply[E <: Throwable, A](result: IO[E, A]): Option[StdFuture[A]] = - IO.unapply(result).map(_._1) + def unapply[E <: Throwable, A](result: Task[E, A]): Option[StdFuture[A]] = + Task.unapply(result) - final def apply[A](body: => A)(implicit ec: ExecutionContext): IO[Throwable, A] = - IO[Throwable, A](StdFuture(body), fatal = false) + final def apply[A](body: => A)(implicit ec: ExecutionContext): Task[Throwable, A] = + Task[Throwable, A](StdFuture(body)) - final def fromFuture[A](future: StdFuture[A]): IO[Throwable, A] = - IO.fromFuture(future) + final def fromFuture[A](future: StdFuture[A]): Task[Throwable, A] = + Task.fromFuture(future) - final def fromEither[E <: Throwable, A](either: Either[E, A]): IO[E, A] = - IO.fromEither(either) + final def fromEither[E <: Throwable, A](either: Either[E, A]): Task[E, A] = + Task.fromEither(either) - final def fromTry[A](body: Try[A]): IO[Throwable, A] = - IO.fromTry(body) + final def fromTry[A](body: Try[A]): Task[Throwable, A] = + Task.fromTry(body) - final def successful[A](value: A): IO[Nothing, A] = - IO.successful(value) + final def successful[A](value: A): Task[Nothing, A] = + Task.successful(value) - final def failed[E <: Exception](exception: E): IO[E, Nothing] = - IO.failed(exception) + final def failed[E <: Exception](exception: E): Task[E, Nothing] = + Task.failed(exception) - final def sequence[E <: Throwable, A](results: Seq[IO[E, A]])(implicit + final def sequence[E <: Throwable, A](results: Seq[Task[E, A]])(implicit ec: ExecutionContext - ): IO[E, Seq[A]] = - IO.sequence(results) + ): Task[E, Seq[A]] = + Task.sequence(results) - val unit = IO.unit + val unit = Task.unit } diff --git a/result/src/main/scala/io/github/ragazoor/IO.scala b/result/src/main/scala/io/github/ragazoor/IO.scala deleted file mode 100644 index bf3f671..0000000 --- a/result/src/main/scala/io/github/ragazoor/IO.scala +++ /dev/null @@ -1,174 +0,0 @@ -package io.github.ragazoor - -import io.github.ragazoor.IOUtils.{ failedFailure, zipWithTuple2Fun } - -import scala.concurrent.ExecutionContext.parasitic -import scala.concurrent.duration.Duration -import scala.concurrent.{ Awaitable, CanAwait, ExecutionContext, Future => StdFuture, TimeoutException } -import scala.util.control.NonFatal -import scala.util.{ Failure, Success, Try } - -sealed trait IO[+E <: Throwable, +A] extends Awaitable[A] { - self => - def toFuture: StdFuture[A] - val isFatal: Boolean - - def value: Option[Try[A]] = self.toFuture.value - - def map[B](f: A => B)(implicit ec: ExecutionContext): IO[E, B] = - IO(self.toFuture.transform(_ map f), isFatal) - - def mapTry[B](f: A => Try[B])(implicit ec: ExecutionContext): IO[Throwable, B] = - IO(self.toFuture.transform(_ flatMap f), isFatal) - - def mapEither[E2 >: E <: Throwable, B](f: A => Either[E2, B])(implicit ec: ExecutionContext): IO[E2, B] = - IO(self.toFuture.transform(_ flatMap (f(_).toTry)), isFatal) - - def flatMap[E2 >: E <: Throwable, B](f: A => IO[E2, B])(implicit ec: ExecutionContext): IO[E2, B] = - IO(self.toFuture.flatMap(f(_).toFuture), isFatal) - - def flatten[E2 >: E <: Throwable, B](implicit ev: A <:< IO[E2, B]): IO[E2, B] = - flatMap(ev)(parasitic) - - def mapError[E2 <: Throwable](f: E => E2)(implicit ec: ExecutionContext): IO[E2, A] = { - var isFutureFatal = isFatal - val transformedFuture = self.toFuture.transform { - case Failure(e) if NonFatal(e) && !isFatal => Failure(f(e.asInstanceOf[E])) - case Failure(e) if !NonFatal(e) || isFatal => - isFutureFatal = true - Failure(e) - case success => success - } - IO[E2, A](transformedFuture, isFutureFatal) - } - - def zip[E2 >: E <: Throwable, B](that: IO[E2, B]): IO[E2, (A, B)] = - zipWith(that)(zipWithTuple2Fun)(parasitic) - - def zipWith[E2 >: E <: Throwable, U, R](that: IO[E2, U])(f: (A, U) => R)(implicit - ec: ExecutionContext - ): IO[E2, R] = - IO(self.toFuture.zipWith(that.toFuture)(f), isFatal || that.isFatal) - - def catchAll[E2 >: E <: Throwable, A2 >: A](f: E => IO[E2, A2])(implicit - ec: ExecutionContext - ): IO[E2, A2] = { - var isFutureFatal = isFatal - val transformedFuture = self.toFuture.transformWith { - case Failure(e) if NonFatal(e) && !isFatal => f(e.asInstanceOf[E]).toFuture - case Failure(e) if !NonFatal(e) || isFatal => - isFutureFatal = true - self.toFuture - case _ => self.toFuture - } - IO[E2, A2](transformedFuture, isFutureFatal) - } - - def catchSome[E2 >: E <: Throwable, A2 >: A](pf: PartialFunction[E, IO[E2, A2]])(implicit - ec: ExecutionContext - ): IO[E2, A2] = { - val transformedFuture = self.toFuture.transformWith { - case Failure(e) if NonFatal(e) && pf.isDefinedAt(e.asInstanceOf[E]) && !isFatal => - pf(e.asInstanceOf[E]).toFuture - case _ => - self.toFuture - } - IO[E2, A2](transformedFuture, isFatal) - } - - private final def failedFun[B](v: Try[B]): Try[E] = - v match { - case Failure(e) if NonFatal(e) && !isFatal => Success(e.asInstanceOf[E]) - case Failure(exception) => Failure(exception) - case Success(_) => failedFailure - } - - def failed: IO[NoSuchElementException, E] = - transform(failedFun)(parasitic).asInstanceOf[IO[NoSuchElementException, E]] - - def foreach[U](f: A => U)(implicit executor: ExecutionContext): Unit = - onComplete(_ foreach f) - - def onComplete[B](f: Try[A] => B)(implicit executor: ExecutionContext): Unit = - self.toFuture.onComplete(f) - - def isCompleted: Boolean = self.toFuture.isCompleted - - def transform[B](f: Try[A] => Try[B])(implicit executor: ExecutionContext): IO[Throwable, B] = - IO(self.toFuture.transform(f), isFatal) - - def transformWith[E2 >: E <: Throwable, B](f: Try[A] => IO[E2, B])(implicit - executor: ExecutionContext - ): IO[E2, B] = - IO(self.toFuture.transformWith(f(_).toFuture), isFatal) - - @throws(classOf[TimeoutException]) - @throws(classOf[InterruptedException]) - def ready(atMost: Duration)(implicit permit: CanAwait): this.type = { - self.toFuture.ready(atMost) - this - } - - @throws(classOf[TimeoutException]) - @throws(classOf[InterruptedException]) - def result(atMost: Duration)(implicit permit: CanAwait): A = - self.toFuture.result(atMost) - - def recover[B >: A](pf: PartialFunction[E, B])(implicit executor: ExecutionContext): IO[E, B] = - transform(_.recover(pf.asInstanceOf[PartialFunction[Throwable, B]])) - .asInstanceOf[IO[E, B]] -} - -object IO { - def unapply[E <: Throwable, A](result: IO[E, A]): Option[(StdFuture[A], Boolean)] = - Some((result.toFuture, result.isFatal)) - - private[ragazoor] final def apply[E <: Throwable, A](future: StdFuture[A], fatal: Boolean): IO[E, A] = - new IO[E, A] { - override def toFuture: StdFuture[A] = future - override val isFatal: Boolean = fatal - } - - final def apply[A](body: => A)(implicit ec: ExecutionContext): IO[Throwable, A] = - IO[Throwable, A](StdFuture(body), fatal = false) - - final def fromFuture[A](future: StdFuture[A]): IO[Throwable, A] = - IO(future, fatal = false) - - final def fromEither[E <: Throwable, A](either: Either[E, A]): IO[E, A] = - IO(StdFuture.fromTry(either.toTry), fatal = false) - - final def fromTry[A](result: Try[A]): IO[Throwable, A] = - IO[Throwable, A](StdFuture.fromTry(result), fatal = false) - - final def successful[A](result: A): IO[Nothing, A] = { - val future = StdFuture.successful(result) - new IO[Nothing, A] { - override def toFuture: StdFuture[A] = future - override val isFatal: Boolean = false - } - } - - final def failed[E <: Exception](exception: E): IO[E, Nothing] = { - val future = StdFuture.failed(exception) - new IO[E, Nothing] { - override def toFuture: StdFuture[Nothing] = future - override val isFatal: Boolean = false - } - } - - final def fatal(exception: Exception): IO[Nothing, Nothing] = { - val future = StdFuture.failed(exception) - new IO[Nothing, Nothing] { - override def toFuture: StdFuture[Nothing] = future - override val isFatal: Boolean = true - } - } - - final def sequence[E <: Throwable, A](results: Seq[IO[E, A]])(implicit - ec: ExecutionContext - ): IO[E, Seq[A]] = - IO(StdFuture.sequence(results.map(_.toFuture)), fatal = false) - - val unit = IO.successful(()) -} diff --git a/result/src/main/scala/io/github/ragazoor/Task.scala b/result/src/main/scala/io/github/ragazoor/Task.scala new file mode 100644 index 0000000..36415a0 --- /dev/null +++ b/result/src/main/scala/io/github/ragazoor/Task.scala @@ -0,0 +1,168 @@ +package io.github.ragazoor + +import io.github.ragazoor.TaskUtils.{ failedFailure, zipWithTuple2Fun } + +import scala.concurrent.ExecutionContext.parasitic +import scala.concurrent.duration.Duration +import scala.concurrent.{ Awaitable, CanAwait, ExecutionContext, Future => StdFuture, TimeoutException } +import scala.util.control.NonFatal +import scala.util.{ Failure, Success, Try } + +sealed trait Task[+E <: Throwable, +A] extends Awaitable[A] { + self => + def toFuture: StdFuture[A] + + def value: Option[Try[A]] = self.toFuture.value + + def map[B](f: A => B)(implicit ec: ExecutionContext): Task[E, B] = + Task(self.toFuture.transform(_ map f)) + + def mapTry[B](f: A => Try[B])(implicit ec: ExecutionContext): Task[Throwable, B] = + Task(self.toFuture.transform(_ flatMap f)) + + def mapEither[E2 >: E <: Throwable, B](f: A => Either[E2, B])(implicit ec: ExecutionContext): Task[E2, B] = + Task(self.toFuture.transform(_ flatMap (f(_).toTry))) + + def flatMap[E2 >: E <: Throwable, B](f: A => Task[E2, B])(implicit ec: ExecutionContext): Task[E2, B] = + Task(self.toFuture.flatMap(f(_).toFuture)) + + def flatten[E2 >: E <: Throwable, B](implicit ev: A <:< Task[E2, B]): Task[E2, B] = + flatMap(ev)(parasitic) + + def mapError[E2 <: Throwable](f: E => E2)(implicit ec: ExecutionContext): Task[E2, A] = { + val transformedFuture = self.toFuture.transform { + case Failure(e) if NonFatal(e) => Failure(f(e.asInstanceOf[E])) + case Failure(e) if !NonFatal(e) => + Failure(e) + case success => success + } + Task[E2, A](transformedFuture) + } + + def zip[E2 >: E <: Throwable, B](that: Task[E2, B]): Task[E2, (A, B)] = + zipWith(that)(zipWithTuple2Fun)(parasitic) + + def zipWith[E2 >: E <: Throwable, U, R](that: Task[E2, U])(f: (A, U) => R)(implicit + ec: ExecutionContext + ): Task[E2, R] = + Task(self.toFuture.zipWith(that.toFuture)(f)) + + def catchAll[E2 >: E <: Throwable, A2 >: A](f: E => Task[E2, A2])(implicit + ec: ExecutionContext + ): Task[E2, A2] = { + val transformedFuture = self.toFuture.transformWith { + case Failure(e) if NonFatal(e) => f(e.asInstanceOf[E]).toFuture + case Failure(e) if !NonFatal(e) => + self.toFuture + case _ => self.toFuture + } + Task[E2, A2](transformedFuture) + } + + def catchSome[E2 >: E <: Throwable, A2 >: A](pf: PartialFunction[E, Task[E2, A2]])(implicit + ec: ExecutionContext + ): Task[E2, A2] = { + val transformedFuture = self.toFuture.transformWith { + case Failure(e) if NonFatal(e) && pf.isDefinedAt(e.asInstanceOf[E]) => + pf(e.asInstanceOf[E]).toFuture + case _ => + self.toFuture + } + Task[E2, A2](transformedFuture) + } + + private final def failedFun[B](v: Try[B]): Try[E] = + v match { + case Failure(e) if NonFatal(e) => Success(e.asInstanceOf[E]) + case Failure(exception) => Failure(exception) + case Success(_) => failedFailure + } + + def failed: Task[NoSuchElementException, E] = + transform(failedFun)(parasitic).asInstanceOf[Task[NoSuchElementException, E]] + + def foreach[U](f: A => U)(implicit executor: ExecutionContext): Unit = + onComplete(_ foreach f) + + def onComplete[B](f: Try[A] => B)(implicit executor: ExecutionContext): Unit = + self.toFuture.onComplete(f) + + def isCompleted: Boolean = self.toFuture.isCompleted + + def transform[B](f: Try[A] => Try[B])(implicit executor: ExecutionContext): Task[Throwable, B] = + Task(self.toFuture.transform(f)) + + def transformWith[E2 >: E <: Throwable, B](f: Try[A] => Task[E2, B])(implicit + executor: ExecutionContext + ): Task[E2, B] = { + val transformedFuture = toFuture.transformWith { value => + val newAttempt = f(value) + newAttempt.toFuture + } + Task(transformedFuture) + } + + @throws(classOf[TimeoutException]) + @throws(classOf[InterruptedException]) + def ready(atMost: Duration)(implicit permit: CanAwait): this.type = { + self.toFuture.ready(atMost) + this + } + + @throws(classOf[TimeoutException]) + @throws(classOf[InterruptedException]) + def result(atMost: Duration)(implicit permit: CanAwait): A = + self.toFuture.result(atMost) + + def recover[B >: A](pf: PartialFunction[Throwable, B])(implicit executor: ExecutionContext): Task[E, B] = + Task(toFuture.recover(pf)).asInstanceOf[Task[E, B]] + + def recoverWith[B >: A](pf: PartialFunction[Throwable, Task[Throwable, B]])(implicit + executor: ExecutionContext + ): Task[Throwable, B] = + Task(toFuture.recoverWith(pf.andThen(_.toFuture))) + +} + +object Task { + def unapply[E <: Throwable, A](result: Task[E, A]): Option[StdFuture[A]] = + Some(result.toFuture) + + private[ragazoor] final def apply[E <: Throwable, A](future: StdFuture[A]): Task[E, A] = + new Task[E, A] { + override def toFuture: StdFuture[A] = future + } + + final def apply[A](body: => A)(implicit ec: ExecutionContext): Task[Throwable, A] = + Task[Throwable, A](StdFuture(body)) + + final def fromFuture[A](future: StdFuture[A]): Task[Throwable, A] = + Task(future) + + final def fromEither[E <: Throwable, A](either: Either[E, A]): Task[E, A] = + Task(StdFuture.fromTry(either.toTry)) + + final def fromTry[A](result: Try[A]): Task[Throwable, A] = + Task[Throwable, A](StdFuture.fromTry(result)) + + final def successful[A](result: A): Task[Nothing, A] = { + val future = StdFuture.successful(result) + new Task[Nothing, A] { + override def toFuture: StdFuture[A] = future + } + } + + final def failed[E <: Exception](exception: E): Task[E, Nothing] = { + val future = StdFuture.failed(exception) + new Task[E, Nothing] { + override def toFuture: StdFuture[Nothing] = future + } + } + + final def sequence[E <: Throwable, A](results: Seq[Task[E, A]])(implicit + ec: ExecutionContext + ): Task[E, Seq[A]] = + Task(StdFuture.sequence(results.map(_.toFuture))) + + val unit = Task.successful(()) +} diff --git a/result/src/main/scala/io/github/ragazoor/IOUtils.scala b/result/src/main/scala/io/github/ragazoor/TaskUtils.scala similarity index 96% rename from result/src/main/scala/io/github/ragazoor/IOUtils.scala rename to result/src/main/scala/io/github/ragazoor/TaskUtils.scala index 14101c3..db70c08 100644 --- a/result/src/main/scala/io/github/ragazoor/IOUtils.scala +++ b/result/src/main/scala/io/github/ragazoor/TaskUtils.scala @@ -3,7 +3,7 @@ package io.github.ragazoor import scala.util.Failure import scala.util.control.NoStackTrace -object IOUtils { +object TaskUtils { private[ragazoor] final val failedFailure = Failure[Nothing]( new NoSuchElementException("Future.failed not completed with error E.") with NoStackTrace diff --git a/result/src/main/scala/io/github/ragazoor/implicits.scala b/result/src/main/scala/io/github/ragazoor/implicits.scala index f5e72dd..b7e5a53 100644 --- a/result/src/main/scala/io/github/ragazoor/implicits.scala +++ b/result/src/main/scala/io/github/ragazoor/implicits.scala @@ -3,8 +3,8 @@ package io.github.ragazoor import scala.concurrent.{ Future => StdFuture } object implicits { - implicit class StdFutureToIo[A](val future: StdFuture[A]) { - def io: IO[Throwable, A] = IO.fromFuture(future) + implicit class StdFutureToTask[A](val future: StdFuture[A]) { + def toTask: Task[Throwable, A] = Task.fromFuture(future) } } diff --git a/result/src/main/scala/io/github/ragazoor/migration/IoToStdFuture.scala b/result/src/main/scala/io/github/ragazoor/migration/TaskToStdFuture.scala similarity index 86% rename from result/src/main/scala/io/github/ragazoor/migration/IoToStdFuture.scala rename to result/src/main/scala/io/github/ragazoor/migration/TaskToStdFuture.scala index 070d032..63cd733 100644 --- a/result/src/main/scala/io/github/ragazoor/migration/IoToStdFuture.scala +++ b/result/src/main/scala/io/github/ragazoor/migration/TaskToStdFuture.scala @@ -1,85 +1,85 @@ package io.github.ragazoor.migration -import io.github.ragazoor.IO +import io.github.ragazoor.Task import scala.concurrent.{ Future => StdFuture } import scala.language.implicitConversions -trait IoToStdFuture { - implicit def ioToStdFutureF0[E <: Throwable, B](f0: () => IO[E, B]): () => StdFuture[B] = +trait TaskToStdFuture { + implicit def ioToStdFutureF0[E <: Throwable, B](f0: () => Task[E, B]): () => StdFuture[B] = () => f0().toFuture - implicit def ioToStdFutureF1[E <: Throwable, X1, B](f1: X1 => IO[E, B]): X1 => StdFuture[B] = + implicit def ioToStdFutureF1[E <: Throwable, X1, B](f1: X1 => Task[E, B]): X1 => StdFuture[B] = x1 => f1(x1).toFuture - implicit def ioToStdFutureF2[E <: Throwable, X1, X2, B](f2: (X1, X2) => IO[E, B]): (X1, X2) => StdFuture[B] = + implicit def ioToStdFutureF2[E <: Throwable, X1, X2, B](f2: (X1, X2) => Task[E, B]): (X1, X2) => StdFuture[B] = (x1, x2) => f2(x1, x2).toFuture implicit def ioToStdFutureF3[E <: Throwable, X1, X2, X3, B]( - f3: (X1, X2, X3) => IO[E, B] + f3: (X1, X2, X3) => Task[E, B] ): (X1, X2, X3) => StdFuture[B] = (x1, x2, x3) => f3(x1, x2, x3).toFuture implicit def ioToStdFutureF4[E <: Throwable, X1, X2, X3, X4, B]( - f4: (X1, X2, X3, X4) => IO[E, B] + f4: (X1, X2, X3, X4) => Task[E, B] ): (X1, X2, X3, X4) => StdFuture[B] = (x1, x2, x3, x4) => f4(x1, x2, x3, x4).toFuture implicit def ioToStdFutureF5[E <: Throwable, X1, X2, X3, X4, X5, B]( - f5: (X1, X2, X3, X4, X5) => IO[E, B] + f5: (X1, X2, X3, X4, X5) => Task[E, B] ): (X1, X2, X3, X4, X5) => StdFuture[B] = (x1, x2, x3, x4, x5) => f5(x1, x2, x3, x4, x5).toFuture implicit def ioToStdFutureF6[E <: Throwable, X1, X2, X3, X4, X5, X6, B]( - f6: (X1, X2, X3, X4, X5, X6) => IO[E, B] + f6: (X1, X2, X3, X4, X5, X6) => Task[E, B] ): (X1, X2, X3, X4, X5, X6) => StdFuture[B] = (x1, x2, x3, x4, x5, x6) => f6(x1, x2, x3, x4, x5, x6).toFuture implicit def ioToStdFutureF7[E <: Throwable, X1, X2, X3, X4, X5, X6, X7, B]( - f7: (X1, X2, X3, X4, X5, X6, X7) => IO[E, B] + f7: (X1, X2, X3, X4, X5, X6, X7) => Task[E, B] ): (X1, X2, X3, X4, X5, X6, X7) => StdFuture[B] = (x1, x2, x3, x4, x5, x6, x7) => f7(x1, x2, x3, x4, x5, x6, x7).toFuture implicit def ioToStdFutureF8[E <: Throwable, X1, X2, X3, X4, X5, X6, X7, X8, B]( - f8: (X1, X2, X3, X4, X5, X6, X7, X8) => IO[E, B] + f8: (X1, X2, X3, X4, X5, X6, X7, X8) => Task[E, B] ): (X1, X2, X3, X4, X5, X6, X7, X8) => StdFuture[B] = (x1, x2, x3, x4, x5, x6, x7, x8) => f8(x1, x2, x3, x4, x5, x6, x7, x8).toFuture implicit def ioToStdFutureF9[E <: Throwable, X1, X2, X3, X4, X5, X6, X7, X8, X9, B]( - f9: (X1, X2, X3, X4, X5, X6, X7, X8, X9) => IO[E, B] + f9: (X1, X2, X3, X4, X5, X6, X7, X8, X9) => Task[E, B] ): (X1, X2, X3, X4, X5, X6, X7, X8, X9) => StdFuture[B] = (x1, x2, x3, x4, x5, x6, x7, x8, x9) => f9(x1, x2, x3, x4, x5, x6, x7, x8, x9).toFuture implicit def ioToStdFutureF10[E <: Throwable, X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, B]( - f10: (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10) => IO[E, B] + f10: (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10) => Task[E, B] ): (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10) => StdFuture[B] = (x1, x2, x3, x4, x5, x6, x7, x8, x9, x10) => f10(x1, x2, x3, x4, x5, x6, x7, x8, x9, x10).toFuture implicit def ioToStdFutureF11[E <: Throwable, X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, B]( - f11: (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11) => IO[E, B] + f11: (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11) => Task[E, B] ): (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11) => StdFuture[B] = (x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11) => f11(x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11).toFuture implicit def ioToStdFutureF12[E <: Throwable, X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, B]( - f12: (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12) => IO[E, B] + f12: (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12) => Task[E, B] ): (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12) => StdFuture[B] = (x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12) => f12(x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12).toFuture implicit def ioToStdFutureF13[E <: Throwable, X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, B]( - f13: (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13) => IO[E, B] + f13: (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13) => Task[E, B] ): (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13) => StdFuture[B] = (x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13) => f13(x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13).toFuture implicit def ioToStdFutureF14[E <: Throwable, X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, B]( - f14: (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14) => IO[E, B] + f14: (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14) => Task[E, B] ): (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14) => StdFuture[B] = (x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14) => f14(x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14).toFuture implicit def ioToStdFutureF15[E <: Throwable, X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15, B]( - f15: (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15) => IO[E, B] + f15: (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15) => Task[E, B] ): (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15) => StdFuture[B] = (x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15) => f15(x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15).toFuture @@ -104,7 +104,7 @@ trait IoToStdFuture { X16, B ]( - f16: (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15, X16) => IO[E, B] + f16: (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15, X16) => Task[E, B] ): (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15, X16) => StdFuture[B] = (x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16) => f16(x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16).toFuture @@ -130,7 +130,7 @@ trait IoToStdFuture { X17, B ]( - f17: (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15, X16, X17) => IO[E, B] + f17: (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15, X16, X17) => Task[E, B] ): (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15, X16, X17) => StdFuture[B] = (x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17) => f17(x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17).toFuture @@ -157,7 +157,7 @@ trait IoToStdFuture { X18, B ]( - f18: (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15, X16, X17, X18) => IO[E, B] + f18: (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15, X16, X17, X18) => Task[E, B] ): (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15, X16, X17, X18) => StdFuture[B] = (x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18) => f18(x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18).toFuture @@ -185,7 +185,7 @@ trait IoToStdFuture { X19, B ]( - f19: (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15, X16, X17, X18, X19) => IO[E, B] + f19: (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15, X16, X17, X18, X19) => Task[E, B] ): (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15, X16, X17, X18, X19) => StdFuture[B] = (x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18, x19) => f19(x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18, x19).toFuture @@ -214,7 +214,7 @@ trait IoToStdFuture { X20, B ]( - f20: (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15, X16, X17, X18, X19, X20) => IO[E, B] + f20: (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15, X16, X17, X18, X19, X20) => Task[E, B] ): (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15, X16, X17, X18, X19, X20) => StdFuture[B] = (x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18, x19, x20) => f20(x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18, x19, x20).toFuture @@ -244,7 +244,29 @@ trait IoToStdFuture { X21, B ]( - f21: (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15, X16, X17, X18, X19, X20, X21) => IO[E, B] + f21: ( + X1, + X2, + X3, + X4, + X5, + X6, + X7, + X8, + X9, + X10, + X11, + X12, + X13, + X14, + X15, + X16, + X17, + X18, + X19, + X20, + X21 + ) => Task[E, B] ): (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15, X16, X17, X18, X19, X20, X21) => StdFuture[B] = (x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18, x19, x20, x21) => f21(x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18, x19, x20, x21).toFuture @@ -298,7 +320,7 @@ trait IoToStdFuture { X20, X21, X22 - ) => IO[E, B] + ) => Task[E, B] ): (X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15, X16, X17, X18, X19, X20, X21, X22) => StdFuture[ B ] = diff --git a/result/src/main/scala/io/github/ragazoor/migration/implicits.scala b/result/src/main/scala/io/github/ragazoor/migration/implicits.scala index 052e260..564395d 100644 --- a/result/src/main/scala/io/github/ragazoor/migration/implicits.scala +++ b/result/src/main/scala/io/github/ragazoor/migration/implicits.scala @@ -1,12 +1,13 @@ package io.github.ragazoor.migration +import io.github.ragazoor.Task + import scala.concurrent.{ Future => StdFuture } import scala.language.implicitConversions -import io.github.ragazoor.IO -object implicits extends IoToStdFuture { +object implicits extends TaskToStdFuture { - implicit def ioToStdFuture[E <: Throwable, A](io: IO[E, A]): StdFuture[A] = + implicit def ioToStdFuture[E <: Throwable, A](io: Task[E, A]): StdFuture[A] = io.toFuture } diff --git a/result/src/main/scala/io/github/ragazoor/package.scala b/result/src/main/scala/io/github/ragazoor/package.scala index fad07e2..c645744 100644 --- a/result/src/main/scala/io/github/ragazoor/package.scala +++ b/result/src/main/scala/io/github/ragazoor/package.scala @@ -1,5 +1,5 @@ package io.github package object ragazoor { - type Future[+A] = IO[Throwable, A] + type Future[+A] = Task[Throwable, A] } diff --git a/result/src/test/scala/io/github/ragazoor/FutureToIOSpec.scala b/result/src/test/scala/io/github/ragazoor/FutureToTaskSpec.scala similarity index 81% rename from result/src/test/scala/io/github/ragazoor/FutureToIOSpec.scala rename to result/src/test/scala/io/github/ragazoor/FutureToTaskSpec.scala index 9ce0bf1..014ae46 100644 --- a/result/src/test/scala/io/github/ragazoor/FutureToIOSpec.scala +++ b/result/src/test/scala/io/github/ragazoor/FutureToTaskSpec.scala @@ -5,14 +5,14 @@ import munit.FunSuite import scala.concurrent.{ Future => StdFuture } -class FutureToIOSpec extends FunSuite { +class FutureToTaskSpec extends FunSuite { import scala.concurrent.ExecutionContext.Implicits.global override def munitValueTransforms = super.munitValueTransforms ++ List( new ValueTransform( - "IO", - { case IO(future, _) => + "Attempt", + { case Task(future) => future } ) @@ -22,7 +22,7 @@ class FutureToIOSpec extends FunSuite { result <- StdFuture(1) } yield assert(result == 1) - result.io + result.toTask } } diff --git a/result/src/test/scala/io/github/ragazoor/IOSpec.scala b/result/src/test/scala/io/github/ragazoor/IOSpec.scala deleted file mode 100644 index 3c9faa9..0000000 --- a/result/src/test/scala/io/github/ragazoor/IOSpec.scala +++ /dev/null @@ -1,210 +0,0 @@ -package io.github.ragazoor - -import munit.FunSuite - -import scala.concurrent.{ Future => StdFuture } -import scala.util.Try - -class IOSpec extends FunSuite { - - import scala.concurrent.ExecutionContext.Implicits.global - - override def munitValueTransforms = super.munitValueTransforms ++ List( - new ValueTransform( - "TypedFuture", - { case IO(future, _) => future } - ) - ) - - abstract class RootError(e: Throwable) extends Exception(e) - - case class MyError(e: Throwable) extends RootError(e) - - case class MyError2(e: Throwable) extends RootError(e) - - case class MyError3(e: Throwable) extends RootError(e) - - test("Typed Future using flatmap") { - for { - a <- IO(1) - b <- IO.fromFuture(StdFuture(2)) - c <- IO.fromTry(Try(3)) - d <- IO.fromEither(Right(4)) - } yield assertEquals(a + b + c + d, 10) - } - - test("Typed Future using flatmap with typed errors") { - val a = for { - a <- IO(1) - b <- IO(2) - _ <- IO.failed(MyError(new RuntimeException("test message"))) - } yield assertEquals(a + b, 3) - a.catchSome { - case _: RuntimeException => IO.successful(assert(false)) - case _: MyError => IO.successful(assert(true)) - } - } - - test("Typed Future using sequence") { - def getResult(i: Int) = IO(i) - - IO - .sequence(Seq(1, 2, 3).map(getResult)) - .map(_.sum) - .map(sum => assertEquals(sum, 6)) - } - - test("Typed Future fail with typed error") { - def getResult(i: Int) = - if (i < 2) - IO.successful(i) - else - IO.failed(new IllegalArgumentException("test message")) - - IO - .sequence(Seq(1, 2, 3).map(getResult)) - .catchSome { case _: IllegalArgumentException => - IO.successful(assert(true)) - } - } - - test("Typed Future.fromEither fail with typed error") { - IO.fromEither(Left(new RuntimeException("Test message"))).catchSome { case _: RuntimeException => - IO.successful(assert(true)) - } - } - - test("Typed Future using flatten") { - val result1 = IO(1) - val result2 = IO(result1) - result2.flatten.catchSome { case _: IllegalArgumentException => - IO.successful(assert(true)) - } - } - - test("Typed Future apply works") { - val result = IO(1) - result.map(one => assertEquals(one, 1)) - } - - test("Typed Future can fail using apply") { - def failingFunc(): Unit = throw new RuntimeException("test message") - - val result = IO[Unit](failingFunc()).mapError(MyError.apply) - result.catchSome { case _: MyError => - IO.successful(assert(true)) - } - } - - test("Typed Future using zip") { - val result1 = IO(1) - val result2 = IO(2) - result1.zip(result2).map { tuple => - assertEquals(tuple, (1, 2)) - } - } - - test("Typed Future catchAll") { - val a = for { - a <- IO.failed(MyError2(new IllegalArgumentException("Bad argument"))) - _ <- IO.failed(MyError(new RuntimeException("test message"))) - } yield assertEquals(a, -1) - a.catchAll { _ => - IO.successful(assert(true)) - } - } - - test("CatchSome does not catch errors not specified") { - IO - .failed(new RuntimeException("Test message")) - .catchSome { case _: IllegalArgumentException => - IO.successful(assert(false)) - } - .catchSome { case _: RuntimeException => - IO.successful(assert(true)) - } - } - - test("Typed Future cannot catch fatal errors") { - val fatalIO = for { - _ <- IO.fatal(new RuntimeException("Killing the process")) - } yield assert(false) - val isFatal = fatalIO.isFatal - fatalIO - .catchAll(_ => IO.successful(assert(false))) - .toFuture - .recover { case _: RuntimeException => - assert(isFatal) - } - } - - test("Future.failed returns error") { - IO - .failed(new IllegalArgumentException("Bad argument")) - .failed - .map(e => assertEquals(e.getMessage, "Bad argument")) - } - - test("Future.failed fails with NoSuchElementException if it is a success") { - IO - .successful(1) - .failed - .map(_ => assert(false)) - .catchSome { case _: NoSuchElementException => - IO.successful(assert(true)) - } - } - - test("Future.failed fails with FatalError if it contains a FatalError") { - val fatalIO = IO - .fatal(new RuntimeException("Test message")) - .failed - .map(_ => assert(false)) - .catchSome { case _: NoSuchElementException => - IO.successful(assert(false)) - } - val isFatal = fatalIO.isFatal - fatalIO.toFuture.recover { case _: RuntimeException => - assert(isFatal) - } - } - - test("Future.mapEither") { - IO.successful(1) - .mapEither(_ => Right(2)) - .map(result => assert(result == 2)) - } - - test("Future.mapEither, with typed error") { - IO.successful(1) - .mapEither[RuntimeException, Int](_ => Left(new RuntimeException("Test message"))) - .map(_ => assert(false)) - .catchSome { case _: RuntimeException => - IO.successful(assert(true)) - } - } - - test("Future.mapTry") { - IO.successful(1) - .mapTry(_ => Try(2)) - .map(result => assert(result == 2)) - } - - test("IO mapError cannot catch fatal errors") { - val result = for { - _ <- IO.fatal(new RuntimeException("Test message")) - } yield assert(false) - - result - .mapError(MyError.apply) - .catchSome { case _: MyError => - IO.successful(assert(false)) - } - .toFuture - .recover { - case _: RuntimeException => assert(true) - case e => assert(e.getMessage == "Test message") - } - - } -} diff --git a/result/src/test/scala/io/github/ragazoor/TaskSpec.scala b/result/src/test/scala/io/github/ragazoor/TaskSpec.scala new file mode 100644 index 0000000..a509e96 --- /dev/null +++ b/result/src/test/scala/io/github/ragazoor/TaskSpec.scala @@ -0,0 +1,169 @@ +package io.github.ragazoor + +import munit.FunSuite + +import scala.concurrent.{ Future => StdFuture } +import scala.util.Try + +class TaskSpec extends FunSuite { + + import scala.concurrent.ExecutionContext.Implicits.global + + override def munitValueTransforms = super.munitValueTransforms ++ List( + new ValueTransform( + "Attempt", + { case Task(future) => future } + ) + ) + + abstract class RootError(e: Throwable) extends Exception(e) + + case class MyError(e: Throwable) extends RootError(e) + + case class MyError2(e: Throwable) extends RootError(e) + + case class MyError3(e: Throwable) extends RootError(e) + + test("Attempt using flatmap") { + for { + a <- Task(1) + b <- Task.fromFuture(StdFuture(2)) + c <- Task.fromTry(Try(3)) + d <- Task.fromEither(Right(4)) + } yield assertEquals(a + b + c + d, 10) + } + + test("Attempt using flatmap with typed errors") { + val a = for { + a <- Task(1) + b <- Task(2) + _ <- Task.failed(MyError(new RuntimeException("test message"))) + } yield assertEquals(a + b, 3) + a.catchSome { + case _: RuntimeException => Task.successful(assert(false)) + case _: MyError => Task.successful(assert(true)) + } + } + + test("Attempt using sequence") { + def getResult(i: Int) = Task(i) + + Task + .sequence(Seq(1, 2, 3).map(getResult)) + .map(_.sum) + .map(sum => assertEquals(sum, 6)) + } + + test("Attempt fail with typed error") { + def getResult(i: Int) = + if (i < 2) + Task.successful(i) + else + Task.failed(new IllegalArgumentException("test message")) + + Task + .sequence(Seq(1, 2, 3).map(getResult)) + .catchSome { case _: IllegalArgumentException => + Task.successful(assert(true)) + } + } + + test("Attempt.fromEither fail with typed error") { + Task.fromEither(Left(new RuntimeException("Test message"))).catchSome { case _: RuntimeException => + Task.successful(assert(true)) + } + } + + test("Attempt using flatten") { + val result1 = Task(1) + val result2 = Task(result1) + result2.flatten.catchSome { case _: IllegalArgumentException => + Task.successful(assert(true)) + } + } + + test("Attempt apply works") { + val result = Task(1) + result.map(one => assertEquals(one, 1)) + } + + test("Attempt can fail using apply") { + def failingFunc(): Unit = throw new RuntimeException("test message") + + val result = Task[Unit](failingFunc()).mapError(MyError.apply) + result.catchSome { case _: MyError => + Task.successful(assert(true)) + } + } + + test("Attempt using zip") { + val result1 = Task(1) + val result2 = Task(2) + result1.zip(result2).map { tuple => + assertEquals(tuple, (1, 2)) + } + } + + test("Attempt catchAll") { + val a = for { + a <- Task.failed(MyError2(new IllegalArgumentException("Bad argument"))) + _ <- Task.failed(MyError(new RuntimeException("test message"))) + } yield assertEquals(a, -1) + a.catchAll { _ => + Task.successful(assert(true)) + } + } + + test("CatchSome does not catch errors not specified") { + Task + .failed(new RuntimeException("Test message")) + .catchSome { case _: IllegalArgumentException => + Task.successful(assert(false)) + } + .catchSome { case _: RuntimeException => + Task.successful(assert(true)) + } + } + + test("Attempt.failed returns error") { + Task + .failed(new IllegalArgumentException("Bad argument")) + .failed + .map(e => assertEquals(e.getMessage, "Bad argument")) + } + + test("Attempt.failed fails with NoSuchElementException if it is a success") { + Task + .successful(1) + .failed + .map(_ => assert(false)) + .catchSome { case _: NoSuchElementException => + Task.successful(assert(true)) + } + } + + test("Attempt.mapEither") { + Task + .successful(1) + .mapEither(_ => Right(2)) + .map(result => assert(result == 2)) + } + + test("Attempt.mapEither, with typed error") { + Task + .successful(1) + .mapEither[RuntimeException, Int](_ => Left(new RuntimeException("Test message"))) + .map(_ => assert(false)) + .catchSome { case _: RuntimeException => + Task.successful(assert(true)) + } + } + + test("Attempt.mapTry") { + Task + .successful(1) + .mapTry(_ => Try(2)) + .map(result => assert(result == 2)) + } + +}