如何使用 Cats IO monad 实现 if-else 逻辑?
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
的 return 类型更改为 IO[Option[UserId]]
,并且不根据电子邮件或用户名区分已经存在的用户,而是将它们视为只是用户已经存在错误,因此我只在左侧使用 String
而不是 NonEmptyList
.
根据安德烈的回答,我为这个用例开发了自己的解决方案。
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
立即终止程序执行流程,稍后可用于描述性响应。
使用 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
的 return 类型更改为 IO[Option[UserId]]
,并且不根据电子邮件或用户名区分已经存在的用户,而是将它们视为只是用户已经存在错误,因此我只在左侧使用 String
而不是 NonEmptyList
.
根据安德烈的回答,我为这个用例开发了自己的解决方案。
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
立即终止程序执行流程,稍后可用于描述性响应。