Scala, cats - как создать финальную реализацию без тегов с помощью IO (или другой монады) и Either? - PullRequest
2 голосов
/ 08 ноября 2019

Я создал простой trait и его реализацию:

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
}

Здесь я хотел бы использовать Either или EitherT, чтобы "ловить" ошибки, но я также хотел бы использоватьIO или Future в качестве главной монады. В моем основном классе я создал вызов этой реализации:

 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")
}

Но в строке, где создается pointer, IntelliJ показывает мне ошибку: Type mismatch - required: UserRepositoryAlg[F], found: UserRepositoryInterpreter, и я не понимаю, почему. Я создал Pointer класс с F[_] как IO и хочу использовать реализацию UserRepositoryAlg[F]. Как я мог решить эту проблему или что является хорошей практикой в ​​этом случае? Если я хочу добиться чего-то вроде этого: IO[Either[Error, User]] или EitherT[IO, Error, User].

Я пытался изменить class UserRepositoryInterpreter extends UserRepositoryAlg[Either[Error, *]] на что-то вроде class UserRepositoryInterpreter[F[_]] extends UserRepositoryAlg[F[Either[Error, *]]], но это мне не помогло.

РЕДАКТИРОВАТЬ: Я узнал, как вернуть F[Either[Error,User]] с помощью Applicative[F], которые преобразуют 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
}

Но у меня все еще есть проблема в основной функции, потому что яне может получить Right значение 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)
    }

Здесь updated <- EitherT.rightT(repo.update(user)) user равно Either[Error, User], но мне нужно передать только User. Поэтому я попытался сделать что-то вроде: Right(user).map(u=>u) и передать его, но это также не помогает. Как мне принять это значение?

1 Ответ

3 голосов
/ 08 ноября 2019

F[_] описывает ваш основной эффект. Теоретически, вы можете использовать любую монаду (или даже любой тип с более высоким родом), но на практике лучшим выбором является монада, которая позволяет вам приостановить выполнение, например cats-effect или Future.

* 1005. * Ваша проблема в том, что вы пытаетесь использовать IO в качестве основного эффекта, но для UserRepositoryInterpreter ваш набор Either в качестве F.

Что вам нужно сделать, это просто параметризовать UserRepositoryInterpreter, чтобы вы могли выбрать монаду эффекта. Если вы хотите использовать Either для обработки ошибок и F для приостановки эффектов, вам следует использовать стек монад F[Either[Error, User]].

Пример решения:

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

}
...