Search code examples
scalascalazmonad-transformersio-monad

IO and Future[Option] monad transformers


I'm trying to figure out how to write this piece of code in an elegant pure-functional style using scalaz7 IO and monad transformers but just can't get my head around it.

Just imagine I have this simple API:

def findUuid(request: Request): Option[String] = ???
def findProfile(uuid: String): Future[Option[Profile]] = redisClient.get[Profile](uuid)

Using this API I can easily write impure function with OptionT transformer like this:

val profileT = for {
  uuid <- OptionT(Future.successful(findUuid(request)))
  profile <- OptionT(findProfile(uuid))
} yield profile
val profile: Future[Option[Profile]] = profileT.run

As you have noticed - this function contains findProfile() with a side-effect. I want to isolate this effect inside of the IO monad and interpret outside of the pure function but don't know how to combine it all together.

def findProfileIO(uuid: String): IO[Future[Option[Profile]]] = IO(findProfile(uuid))

val profileT = for {
  uuid <- OptionT(Future.successful(findUuid(request)))
  profile <- OptionT(findProfileIO(uuid)) //??? how to put Option inside of the IO[Future[Option]]
} yield profile
val profile = profileT.run //how to run transformer and interpret IO with the unsafePerformIO()??? 

Any peaces of advice on how it might be done?


Solution

  • IO is meant more for synchronous effects. Task is more what you want! See this question and answer: What's the difference between Task and IO in Scalaz?

    You can convert your Future to Task and then have an API like this:

    def findUuid(request: Request): Option[String] = ??? 
    def findProfile(uuid: String): Task[Option[Profile]] = ???
    

    This works because Task can represent both synchronous and asynchronous operations, so findUuid can also be wrapped in Task instead of IO.

    Then you can wrap these in OptionT:

    val profileT = for {
      uuid <- OptionT(Task.now(findUuid(request)))
      profile <- OptionT(findProfileIO(uuid))
    } yield profile
    

    Then at the end somewhere you can run it:

    profileT.run.attemptRun
    

    Check out this link for converting Futures to Tasks and vice versa: Scalaz Task <-> Future