What is a correct way to implement if-else logic with Cats IO monad?
Here is a basic example with user registration flow described with pseudo code:
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)
}
How to implement the same logic in terms of 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]]
Since you want a NonEmptyList
of errors, it seems that you have to combine the results of getUserByUsername
and getUserByEmail
with Validated
, and only later convert it into an Either
. On this Either
, you can then invoke a fold
with some IO
s in both branches. It was too awkward to combine it in one for
-comprehension, so I separated it into two methods:
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
}
}
Something like this maybe?
Or alternatively with 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
}
or:
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
}