繁体   English   中英

如何使用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的返回类型更改为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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM