【问题标题】:Scala, cats - how to create tagless-final implementation with IO (or other monad) and Either?Scala,猫 - 如何使用 IO(或其他 monad)和 Either 创建无标记的最终实现?
【发布时间】:2020-03-05 00:50:37
【问题描述】:

我创建了一个简单的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
}

在这里,我想使用EitherEitherT 来“捕获”错误,但我也想使用IOFuture 作为主monad。在我的主类中,我创建了对这个实现的调用:

 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,我不明白为什么。我用F[_] 创建了Pointer 类作为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, *]]] 之类的内容,但没有帮助。

编辑: 我发现了如何使用 Applicative[F] 转换 A =&gt; F[A] 来返回 F[Either[Error,User]]

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
}

但是我的main函数还是有问题,因为我无法获取EitherRight值:

 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 &lt;- EitherT.rightT(repo.update(user))userEither[Error, User],但我只需要通过User。所以我尝试做类似的事情: Right(user).map(u=&gt;u) 并通过它,但它也无济于事。我应该如何取这个值?

【问题讨论】:

    标签: scala functional-programming scala-cats tagless-final


    【解决方案1】:

    F[_] 描述了你的主要作用。理论上,您可以使用任何 monad(甚至任何更高种类的类型),但在实践中,最好的选择是 monad,它允许您像 cats-effectFuture 那样暂停执行。

    你的问题是你试图使用IO作为你的主要效果,但是对于UserRepositoryInterpreter你设置Either作为你的F

    你应该做的只是参数化UserRepositoryInterpreter,你可以选择你的效果单子。如果你想同时使用Either 处理错误和F 暂停效果,你应该使用monad stack 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
    
    }
    

    【讨论】:

    • 谢谢。看起来很有趣,我会分析它。另外你认为,做类似F[Either[Error, Data]] 的事情是一个好习惯,还是应该重写成别的东西?
    • 这是一种非常普遍的做法。实际上EitherT 是一个单子转换器,用于简化对此类嵌套单子的操作。如果您使用 effect monad,还有另一种处理错误的方法。您可以使用来自MonadError 类型类的raiseError。在这种情况下,您不需要嵌套 monad(例如,您的类型将是 F[User])。
    • 谢谢。 Sync[F].delay(...) 有语法糖吗?通过将其用作隐式或其他方式?
    • 例如,您可以将UserRepositoryInterpreter 定义为class UserRepositoryInterpreter[F[_]](implicit S: Sync[F])(有时隐式参数在描述类型类时使用大写字母定义,您也可以使用小写字母ssync)然后像S.delay 一样使用它。但实际上符号Sync[F].delay 很常见。请查看此cats effect tutorial。实际上用符号Sync[F] 来解决隐式问题是使用implicit Summoner 调用的,我承认一开始它看起来有点奇怪:)
    • 感谢您的帮助。会看这个教程。
    猜你喜欢
    • 2019-11-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-14
    • 1970-01-01
    • 2019-11-11
    • 2012-06-02
    相关资源
    最近更新 更多