Eugene Zhulkov
Eugene Zhulkov

Reputation: 515

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?

Upvotes: 4

Views: 1019

Answers (2)

Eugene Zhulkov
Eugene Zhulkov

Reputation: 515

End up with this piece of code, thought it might be useful for someone (Play 2.6).

Controller's method is a pure function since Task evaluation takes place outside of the controller inside of PureAction ActionBuilder. Thanks to Luka's answer!

Still struggling with new paradigm of Action composition in Play 2.6 though, but this is another story.

FrontendController.scala:

def index = PureAction.pure { request =>
  val profileOpt = (for {
    uuid <- OptionT(Task.now(request.cookies.get("uuid").map(t => uuidKey(t.value))))
    profile <- OptionT(redis.get[Profile](uuid).asTask)
  } yield profile).run
  profileOpt.map { profileOpt =>
    Logger.info(profileOpt.map(p => s"User logged in - $p").getOrElse("New user, suggesting login"))
    Ok(views.html.index(profileOpt))
  }
}

Actions.scala

Convenient action with Task resolution at the end

class PureAction @Inject()(parser: BodyParsers.Default)(implicit ec: ExecutionContext) extends ActionBuilderImpl(parser) {
  self =>
  def pure(block: Request[AnyContent] => Task[Result]): Action[AnyContent] = composeAction(new Action[AnyContent] {
    override def parser: BodyParser[AnyContent] = self.parser
    override def executionContext: ExecutionContext = self.ec
    override def apply(request: Request[AnyContent]): Future[Result] = {
      val taskResult = block(request)
      taskResult.asFuture //End of the world lives here
    }
  })
}

Converters.scala

Task->Future and Future->Task implicit converters

implicit class FuturePimped[+T](root: => Future[T]) {
  import scalaz.Scalaz._
  def asTask(implicit ec: ExecutionContext): Task[T] = {
    Task.async { register =>
      root.onComplete {
        case Success(v) => register(v.right)
        case Failure(ex) => register(ex.left)
      }
    }
  }
}

implicit class TaskPimped[T](root: => Task[T]) {
  import scalaz._
  val p: Promise[T] = Promise()
  def asFuture: Future[T] = {
    root.unsafePerformAsync {
      case -\/(ex) => p.failure(ex); ()
      case \/-(r) => p.success(r); ()
    }
    p.future
  }
}

Upvotes: 1

Luka Jacobowitz
Luka Jacobowitz

Reputation: 23502

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

Upvotes: 3

Related Questions