如何使用 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的错误,看来你必须将getUserByUsernamegetUserByEmail的结果与Validated结合起来,然后再转换它变成 Either。在这个 Either 上,您可以在两个分支中调用带有一些 IOfold。把它组合成一个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 立即终止程序执行流程,稍后可用于描述性响应。