简体   繁体   中英

How do I abstract over effects and use ContextShift with Scala Cats?

I am creating in Scala and Cats a function that does some I/O and that will be called by other parts of the code. I'm also learning Cats and I want my function to:

  • Be generic in its effect and use a F[_]
  • Run on a dedicated thread pool
  • I want to introduce async boundaries

I assume that all my functions are generic in F[_] up to the main method because I'm trying to follow these Cat's guidelines

But I struggle to make these constraint to work by using ContextShift or ExecutionContext . I have written a full example here and this is an exctract from the example:

object ComplexOperation {
  // Thread pool for ComplexOperation internal use only
  val cs = IO.contextShift(
    ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor())
  )

  // Complex operation that takes resources and time
  def run[F[_]: Sync](input: String): F[String] =
    for {
      r1 <- Sync[F].delay(cs.shift) *> op1(input)
      r2 <- Sync[F].delay(cs.shift) *> op2(r1)
      r3 <- Sync[F].delay(cs.shift) *> op3(r2)
    } yield r3

  def op1[F[_]: Sync](input: String): F[Int]     = Sync[F].delay(input.length)
  def op2[F[_]: Sync](input: Int): F[Boolean]    = Sync[F].delay(input % 2 == 0)
  def op3[F[_]: Sync](input: Boolean): F[String] = Sync[F].delay(s"Complex result: $input")
}

This clearly doesn't abstract over effects as ComplexOperation.run needs a ContextShift[IO] to be able to introduce async boundaries. What is the right (or best) way of doing this?

Creating ContextShift[IO] inside ComplexOperation.run makes the function depend on IO which I don't want. Moving the creation of a ContextShift[IO] on the caller will simply shift the problem: the caller is also generic in F[_] so how does it obtain a ContextShift[IO] to pass to ComplexOperation.run without explicitly depending on IO ?

Remember that I don't want to use one global ContextShift[IO] defined at the topmost level but I want each component to decide for itself.

Should my ComplexOperation.run create the ContextShift[IO] or is it the responsibility of the caller?

Am I doing this right at least? Or am I going against standard practices?

So I took the liberty to rewrite your code, hope it helps:

import cats.effect._

object Functions {
  def sampleFunction[F[_]: Sync : ContextShift](file: String, blocker: Blocker): F[String] = {
    val handler: Resource[F, Int] =
      Resource.make(
        blocker.blockOn(openFile(file))
      ) { handler =>
        blocker.blockOn(closeFile(handler))
      }

    handler.use(handler => doWork(handler))
  }

  private def openFile[F[_]: Sync](file: String): F[Int] = Sync[F].delay {
    println(s"Opening file $file with handler 2")
    2
  }

  private def closeFile[F[_]: Sync](handler: Int): F[Unit] = Sync[F].delay {
    println(s"Closing file handler $handler")
  }

  private def doWork[F[_]: Sync](handler: Int): F[String] = Sync[F].delay {
    println(s"Calculating the value on file handler $handler")
    "The final value" 
  }
}

object Main extends IOApp {
  override def run(args: List[String]): IO[ExitCode] = {
    val result = Blocker[IO].use { blocker =>
      Functions.sampleFunction[IO](file = "filePath", blocker)
    }

    for {
      data <- result
      _ <- IO(println(data))
    } yield ExitCode.Success
  }
}

You can see it running here .

So, what does this code does.
First, it creates a Resource for the file, since close has to be done, even on guarantee or on failure.
It is using Blocker to run the open and close operations on a blocking thread poo (that is done using ContextShift ) .
Finally, on the main, it creates a default Blocker for instance, for **IO*, and uses it to call your function; and prints the result.

Fell free to ask any question.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM