用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]]
答案 0 :(得分:2)
由于您想要NonEmptyList
的错误,因此似乎必须将getUserByUsername
和getUserByEmail
的结果与Validated
合并,然后再将其转换为Either
。然后,您可以在此Either
上调用两个分支中有一些fold
的{{1}}。很难将它组合成一个IO
的理解,所以我将其分为两种方法:
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
}
答案 1 :(得分:2)
考虑以下示例
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
。
答案 2 :(得分:0)
基于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
来立即终止程序执行流,该消息稍后可用于描述性响应。