scalafunctional-programmingscala-catstagless-final

Scala, cats - how to create tagless-final implementation with IO (or other monad) and Either?


I have created a simple trait and his implementation:

trait UserRepositoryAlg[F[_]] {

  def find(nick: String): F[User]

  def update(user: User): F[User]
}

class UserRepositoryInterpreter extends UserRepositoryAlg[Either[Error, *]] {
  override def find(nick: String): Either[Error, User] = for {
    res <- users.find(user => user.nick == nick).toRight(UserError)
  } yield res

  override def update(user: User): Either[Error, User] = for {
    found <- users.find(u => u.nick == user.nick).toRight(UserError)
    updated = found.copy(points = found.points + user.points)
  } yield updated
}

Here I would like to use Either or EitherT to "catch" errors, but I would like also to use IO or Future as a main monad. In my main class I created call to this implementation:

 object Main extends App {

  class Pointer[F[_] : Monad](repo: UserRepositoryAlg[F]) {
    def addPoints(nick: String): EitherT[F, Error, User] = {
      for {
        user <- EitherT.right(repo.find(nick))
        updated <- EitherT.right(repo.update(user))
      } yield Right(updated)
    }
  }
  val pointer = new Pointer[IO](new UserRepositoryInterpreter{}).addPoints("nick")
}

But in the line where pointer is created, IntelliJ shows me an error: Type mismatch - required: UserRepositoryAlg[F], found: UserRepositoryInterpreter and I do not understand why. I created Pointer class with F[_] as a IO and want to use the implementation of UserRepositoryAlg[F]. How I could fix this problem or what is a good practice in this case? If I want to achieve something like this: IO[Either[Error, User]] or EitherT[IO, Error, User].

I tried to change class UserRepositoryInterpreter extends UserRepositoryAlg[Either[Error, *]] into something like class UserRepositoryInterpreter[F[_]] extends UserRepositoryAlg[F[Either[Error, *]]], but it did not help me.

EDIT: I found out how to return F[Either[Error,User]] by using Applicative[F] which transform A => F[A]:

class UserRepositoryInterpreter[F[_] : Applicative] extends UserRepositoryAlg[F[Either[Error, *]]] {
  override def find(nick: String): F[Either[Error, User]] = for {
    res <- Applicative[F].pure(users.find(user => user.nick == nick).toRight(UserError))
  } yield res

  override def update(user: User): F[Either[Error, User]] = for {
    found <- Applicative[F].pure(users.find(u => u.nick == user.nick).toRight(UserError))
    updated = Applicative[F].pure(found.map(u => u.copy(points = u.points + user.points)))
  } yield updated
}

But I still have a problem in the main function, because I cannot get Right value of Either:

 def addPoints(nick: String): EitherT[F, Error, User] = {
      for {
        user <- EitherT.liftF(repo.find(nick))
        updated <- EitherT.rightT(repo.update(user))
      } yield Right(updated)
    }

Here updated <- EitherT.rightT(repo.update(user)) user is Either[Error, User], but I need to pass only User. So I tried to do something like: Right(user).map(u=>u) and pass it but it also does not help. How I should take this value?


Solution

  • F[_] describes your main effect. In theory, you could use any monad(or even any higher-kinded-type), but in practice, the best choice is a monad, that allows you to suspend execution like cats-effect or Future.

    Your problem is that you're trying to use IO as your main effect, but for UserRepositoryInterpreter your set Either as your F.

    What you should do is just parametrize UserRepositoryInterpreter, that you could choose your effect monad. If you want to use both Either for handling errors and F for suspending effects, you should use monad stack F[Either[Error, User]].

    Example solution:

    import cats.Monad
    import cats.data.EitherT
    import cats.effect.{IO, Sync}
    import cats.implicits._
    
    case class User(nick: String, points: Int)
    
    trait UserRepositoryAlg[F[_]] {
    
      def find(nick: String): F[Either[Error, User]]
    
      def update(user: User): F[Either[Error, User]]
    }
    
    //UserRepositoryInterpreter is parametrized, but we require that F has typeclass Sync,
    //which would allow us to delay effects with `Sync[F].delay`.
    //Sync extends Monad, so we don't need to request is explicitly to be able to use for-comprehension
    class UserRepositoryInterpreter[F[_]: Sync] extends UserRepositoryAlg[F] {
    
      val users: mutable.ListBuffer[User] = ListBuffer()
    
      override def find(nick: String): F[Either[Error, User]] = for {
        //Finding user will be delayed, until we interpret and run our program. Delaying execution is useful for side-effecting effects,
        //like requesting data from database, writting to console etc.
        res <- Sync[F].delay(Either.fromOption(users.find(user => user.nick == nick), new Error("Couldn't find user")))
      } yield res
    
    
      //we can reuse find method from UserRepositoryInterpreter, but we have to wrap find in EitherT to access returned user
      override def update(user: User): F[Either[Error, User]] = (for {
        found <- EitherT(find(user.nick))
        updated = found.copy(points = found.points + user.points)
      } yield updated).value
    }
    
    object Main extends App {
    
      class Pointer[F[_] : Monad](repo: UserRepositoryAlg[F]) {
        def addPoints(nick: String): EitherT[F, Error, User] = {
          for {
            user <- EitherT(repo.find(nick))
            updated <- EitherT(repo.update(user))
          } yield updated
        }
      }
    
      //at this point we define, that we want to use IO as our effect monad
      val pointer = new Pointer[IO](new UserRepositoryInterpreter[IO]).addPoints("nick")
    
      pointer.value.unsafeRunSync() //at the end of the world we run our program
    
    }