如何使用Cats IO monad实现if-else逻辑?

时间:2019-06-30 10:32:54

标签: scala scala-cats

用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]]

3 个答案:

答案 0 :(得分:2)

由于您想要NonEmptyList的错误,因此似乎必须将getUserByUsernamegetUserByEmail的结果与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来立即终止程序执行流,该消息稍后可用于描述性响应。