2017-07-03 79 views
4

我想弄清楚如何使用scalaz7 IO和monad變壓器以優雅的純功能風格編寫這段代碼,但僅僅是無法讓我的頭轉向它。IO和未來[選項] monad變壓器

試想一下,我有這個簡單的API:

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

使用這個API,我可以輕鬆地寫出與OptionT變壓器這樣的非純函數:

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

正如你已經注意到了 - 這個函數包含findProfile ()有副作用。我想在IO monad內部隔離這種效應,並在純函數外部解釋,但不知道如何將它們結合在一起。

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()??? 

它可能會如何做諮詢的任何撕成小塊?

回答

3

IO意味着更多的同步效果。 Task更是你想要的! 看到這個問題,回答:What's the difference between Task and IO in Scalaz?

你可以把你FutureTask,然後有一個這樣的API:

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

這工作,因爲Task可以代表同步和異步操作,因此findUuid也可以被包裹在Task而不是IO

然後你就可以在OptionT包裝這些:

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

那麼,在年底的地方,你可以運行它:

profileT.run.attemptRun 

退房此鏈接期貨轉化爲任務,反之亦然:Scalaz Task <-> Future

+0

THX @盧卡 - jacobowitz 有問題 *我用我的Play的Action.async的這裏面的代碼,所以我要回報未來的[結果]。將任務轉換爲scala.Future表示任務終止。一旦終止,Play的操作就會變成同步。你知道如何將任務轉換爲未來而無需終止? *走得更遠 - 你認爲IO Monad只是延期計算嗎?如果是這樣的話,它意味着原始的findProfile:Future也是懶惰的,它的計算也在ExecutionContext中被推遲了?在這種情況下,可能沒有必要將Future包裝到IO中 - 這個功能已經是純粹的了? –

+0

未來的問題是它不是懶惰的。它將執行您在期貨主體內定義的任何副作用,默認情況下使其成爲不純的功能。有關詳情,請參閱此處:https://www.reddit.com/r/scala/comments/3zofjl/why_is_future_totally_unusable/ 我的建議是使用隨處可用的任務,並在需要時轉換爲或來自Future。 –

+0

謝謝您。在昨晚深入挖掘Future/Task比較,請閱讀這篇reddit文章。 –

1

結束這段代碼,認爲它可能對某人有用(Play 2.6)。

控制器的方法是一個純函數,因爲任務評估發生在PureAction ActionBuilder內部的控制器之外。感謝Luka的回答!儘管如此,仍然在Play 2.6的Action範例中掙扎,但這是另一回事。

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

方便行動任務的決議在最後

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

轉換器。斯卡拉

任務 - >未來和未來 - >任務隱含轉換器

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