[英]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.