6

I'm converting Future code to IO. I have code similar to this

def doSomething: Future[Foo] = {
  Future { 
    //some code the result of which we don't care about 
  }
  Future {
    //Foo
  }
}

And then at the end of the program, I doSomething.unsafeRunSync. How do I convert these Futures to IOs while maintaining the fire-and-forget functionality of the first Future? In using IO's async API, I am worried about accidentally blocking the thread when I later call unsafeRunSync on doSomething.

Lasf
  • 2,536
  • 1
  • 16
  • 35
  • The code in your question will effectively ignore the first Future (which will start because that's what Futures do on apply), returning only the second one. Is this the behavior you want to replicate? Have several parallel tasks running in parallel, but caring only about the last one? – mdm May 15 '18 at 16:57
  • @mdm that is correct. The side effect is all that matters in the first. – Lasf May 15 '18 at 16:59

2 Answers2

10

A solution that uses only cats-effect could use IO.start. This, combined with the fact that you will then never join the resulting Fiber, will look something like this:

import cats.effect._
import cats.implicits._    
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._

object ExampleApp extends App{

  val fireAndForget =
    IO(println("Side effect pre-sleep")) *>
      IO.sleep(2.seconds) *>
      IO(println("Side effect post-sleep"))

  val toBeUsed = IO{
    println("Inside second one")
    42
  }

  val result = for {
    fiber <- IO.shift *> fireAndForget.start
    res <- toBeUsed.handleErrorWith { error =>
      // This is just in case you 'toBeUsed' can actually fail,
      // and you might want to cancel the original side-effecting IO
      fiber.cancel *> IO.raiseError(error) }
  } yield res

  println(result.unsafeRunSync())

  println("Waiting 3 seconds...")
  IO.sleep(3.seconds).unsafeRunSync()
  println("Done")
}

This will print (most of the times) something similar to:

Side effect pre-sleep 
Inside second one 
42                       // Up until here, will be printed right away
Waiting 3 seconds...     // It will then be waiting a while
Side effect post-sleep   // ...at which point the side effecting code terminates
Done 

Finally, here are more details about Fiber and IO.shift

mdm
  • 3,928
  • 3
  • 27
  • 43
  • What is the purpose of the `IO.shift` there? – Lasf May 16 '18 at 12:59
  • In the docs (https://typelevel.org/cats-effect/datatypes/io.html#thread-shifting and https://typelevel.org/cats-effect/datatypes/io.html#parallelism) it explains that it is used to specify a "asynchronous boundary". In practice, my understanding is one of the design principle of Cats-effect IO is that everything is explicit, including specifying that a specific async IO will run on a separate "thread": the `shift` makes it explicit. It makes the thread forking explicit. – mdm May 16 '18 at 13:16
  • 2
    From cats-effect 1.0.0 `fireAndForget.start` is forking, so `IO.shift` is useless. – Joan Oct 22 '18 at 12:24
2

I believe that you need to wrap the first Future in such a way that it completes immediately. We ignore exeptions, or catch them however, but they are contained within its own thread. The parameter cb is the promise that needs to complete; so we short-circuit the completion by providing a value immediately.

def firstFuture(implicit ec: ExecutionContext): IO[Unit] = {
  IO.async[Unit] { cb =>
    ec.execute(() => {
      try {
        //some code the result of which we don't care about
      } catch {
      }
    })
    cb(Right(()))
  }
}

In the for-comprehension, the firstFuture will complete immediately even though its thread will have a long-running task active on it.

def doSomething(implicit ec: ExecutionContext): IO[Foo] = {
  for {
    _ <- firstFuture
    IO.async[Foo] { fb =>
      // Foo
    }
  }
}
Lasf
  • 2,536
  • 1
  • 16
  • 35
Bob Dalgleish
  • 8,167
  • 4
  • 32
  • 42
  • I should add that the implicit execution context may complicate issues since you may not want the `firstFuture` to be on the same execution context as the one that calculates `Foo`. In that case, you would pass an explicit execution context as a parameter: `_ <- firstFuture( daemonExecutionContext )` – Bob Dalgleish May 16 '18 at 16:55