[英]How to implement if-else logic with Cats IO monad?
使用Cats IO monad實現if-else邏輯的正確方法是什么?
以下是使用偽代碼描述的用戶注冊流程的基本示例:
registerUser(username, email, password) = {
if (findUser(username) == 1) "username is already in use"
else if (findUser(email) == 1) "email is already in use"
else saveUser(username, email, password)
}
如何在Scala Cats IO monad方面實現相同的邏輯?
def createUser(username: Username, email: Email, password: Password): IO[Unit]
def getUserByUsername(username: Username): IO[Option[User]]
def getUserByEmail(email: Email): IO[Option[User]]
由於您需要NonEmptyList
錯誤,因此您似乎必須將getUserByUsername
和getUserByEmail
的結果與Validated
結合使用,並且稍后才能將其轉換為Either
。 在這個Either
,您可以在兩個分支中調用帶有一些IO
的fold
。 將它組合在一起for
理解是太尷尬了,所以我把它分成兩個方法:
import cats.data.Validated.condNel
import cats.data.NonEmptyList
import cats.syntax.apply._
import cats.syntax.either._
import cats.effect._
case class User(name: String)
trait CreateUserOnlyIfNoCollision {
type Username = String
type Email = String
type Password = String
type ErrorMsg = String
type UserId = Long
def createUser(username: Username, email: Email, password: Password): IO[UserId]
def getUserByUsername(username: Username): IO[Option[User]]
def getUserByEmail(email: Email): IO[Option[User]]
/** Attempts to get user both by name and by email,
* returns `()` if nothing is found, otherwise
* returns a list of error messages that tell whether
* name and/or address are already in use.
*/
def checkUnused(username: Username, email: Email)
: IO[Either[NonEmptyList[String], Unit]] = {
for {
o1 <- getUserByUsername(username)
o2 <- getUserByEmail(email)
} yield {
(
condNel(o1.isEmpty, (), "username is already in use"),
condNel(o2.isEmpty, (), "email is already in use")
).mapN((_, _) => ()).toEither
}
}
/** Attempts to register a user.
*
* Returns a new `UserId` in case of success, or
* a list of errors if the name and/or address are already in use.
*/
def registerUser(username: Username, email: Email, password: Password)
: IO[Either[NonEmptyList[String], UserId]] = {
for {
e <- checkUnused(username, email)
res <- e.fold(
errors => IO.pure(errors.asLeft),
_ => createUser(username, email, password).map(_.asRight)
)
} yield res
}
}
這樣的事可能嗎?
或者使用EitherT
:
def registerUser(username: Username, email: Email, password: Password)
: IO[Either[Nel[String], UserId]] = {
(for {
e <- EitherT(checkUnused(username, email))
res <- EitherT.liftF[IO, Nel[String], UserId](
createUser(username, email, password)
)
} yield res).value
}
要么:
def registerUser(username: Username, email: Email, password: Password)
: IO[Either[Nel[String], UserId]] = {
(for {
e <- EitherT(checkUnused(username, email))
res <- EitherT(
createUser(username, email, password).map(_.asRight[Nel[String]])
)
} yield res).value
}
請考慮以下示例
object So56824136 extends App {
type Error = String
type UserId = String
type Username = String
type Email = String
type Password = String
case class User(name: String)
def createUser(username: Username, email: Email, password: Password): IO[Option[UserId]] = IO { Some("100000001")}
def getUserByUsername(username: Username): IO[Option[User]] = IO { Some(User("picard"))}
def getUserByEmail(email: Email): IO[Option[User]] = IO { Some(User("picard"))}
def userDoesNotAlreadyExists(username: Username, email: Email, password: Password): IO[Either[Error, Unit]] =
(for {
_ <- OptionT(getUserByUsername(username))
_ <- OptionT(getUserByEmail(username))
} yield "User already exists").toLeft().value
def registerUser(username: Username, email: Email, password: Password) : IO[Either[Error, UserId]] =
(for {
_ <- EitherT(userDoesNotAlreadyExists(username, email, password))
id <- OptionT(createUser(username, email, password)).toRight("Failed to create user")
} yield id).value
registerUser("john_doe", "john@example.com", "1111111")
.unsafeRunSync() match { case v => println(v) }
}
哪個輸出
Left(User already exists)
注意我已將createUser
的返回類型更改為IO[Option[UserId]]
,並且不區分基於電子郵件或用戶名已存在的用戶,但將它們視為用戶已存在的錯誤,因此我只使用String
在左邊而不是NonEmptyList
。
根據Andrey的回答,我為這個用例開發了自己的解決方案。
case class User(name: String)
type Username = String
type Email = String
type Password = String
type ErrorMsg = String
type UserId = Long
def createUser(username: Username, email: Email, password: Password): IO[UserId] = ???
def getUserByUsername(username: Username): IO[Option[User]] = ???
def getUserByEmail(email: Email): IO[Option[User]] = ???
def isExist(condition: Boolean)(msg: String): IO[Unit] =
if (condition) IO.raiseError(new RuntimeException(msg)) else IO.unit
def program(username: Username, email: Email, password: Password): IO[Either[String, UserId]] = (for {
resA <- getUserByUsername(username)
_ <- isExist(resA.isDefined)("username is already in use")
resB <- getUserByEmail(email)
_ <- isExist(resB.isDefined)("email is already in use")
userId <- createUser(username, email, password)
} yield {
userId.asRight[String]
}).recoverWith {
case e: RuntimeException => IO.pure(e.getMessage.asLeft[UserId])
}
首先我介紹了一個輔助函數isExist(condition: Boolean)(msg: String): IO[Unit]
。 其目的僅僅是檢查現有用戶名或電子郵件(或其他任何內容)的事實。 除此之外,它還通過拋出帶有適當消息的RuntimeException
立即終止程序執行流程,該消息稍后可用於描述性響應。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.