4

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?

4

2 回答 2

3

IO更多的是用于同步效果。Task是你想要的更多!请参阅此问答:Scalaz 中的 Task 和 IO 有什么区别?

您可以将您的转换FutureTask然后拥有这样的 API:

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

这是可行的,因为Task它可以表示同步和异步操作,所以findUuid也可以TaskIO.

然后你可以将它们包装在OptionT

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

然后在最后你可以运行它的地方:

profileT.run.attemptRun

查看此链接以将 Futures 转换为 Tasks,反之亦然:Scalaz Task <-> Future

于 2017-07-03T15:03:33.893 回答
1

结束这段代码,认为它可能对某人有用(Play 2.6)。

控制器的方法是纯函数,因为任务评估发生在控制器外部的 PureAction ActionBuilder 内。感谢卢卡的回答!

尽管仍在为 Play 2.6 中新的动作组合范例而苦苦挣扎,但这是另一个故事。

前端控制器.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

最后解决任务的方便操作

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
    }
  })
}

转换器.scala

任务->未来和未来->任务隐式转换器

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
  }
}
于 2017-07-06T15:33:58.117 回答